Acceso a datos

Introducción y objetivos

Este módulo profesional amplía la formación necesaria para desempeñar la función de desarrollador de aplicaciones multiplataforma, en la parte de Acceso a Datos. La función de desarrollador de aplicaciones multiplataforma incluye aspectos como:

  • Desarrollo de aplicaciones de gestión de ficheros y directorios.
  • Desarrollo de aplicaciones de acceso a bases de datos relacionales.
  • Desarrollo de aplicaciones que hagan uso de bases de datos orientadas a objetos.
  • Desarrollo de aplicaciones de acceso a bases de datos XML y bases de datos NoSQL, como MongoDB.
  • Desarrollo de componentes de acceso a datos y su integración en aplicaciones.

Las actividades profesionales asociadas a esta función se aplican en el desarrollo de software de gestión multiplataforma con acceso a bases de datos utilizando lenguajes, bibliotecas y herramientas adecuadas las especificaciones.

Todas las empresas en las que los alumnos pueden trabajar al finalizar el ciclo utilizarán alguna tecnología de persistencia de su información. El alumno aprenderá a trabajar con las diferentes tecnologías de persistencia de los datos utilizadas más habitualmente, de forma que pueda adaptarse al entorno existente en el centro de trabajo una vez finalice el ciclo.

Comenzamos

En primer lugar, os quería desear mucho ánimo a todas/os, pues esta materia es la continuación natural de la materia de Programación que habéis cursado en primero, pero centrándonos en la parte del modelo de acceso a los datos. Es el complemento principal de la materia de Desenvolvemento de Interfaces y Programación Multimedia e Dispositivos Móbiles, en la que se trabaja la parte de la vista y el controlador.

Como habéis podido comprobar en la materia de primero, a programar se aprende programando, como con cualquier otra actividad que requiera destreza, como tocar un instrumento, cocinar, un deporte, etc. Seguro que más de un/a sabe a qué me refiero. Por ello intentaremos minimizar la carga teórica para centrarnos en las actividades prácticas y las tareas, eso no implica que no puedan caer cuestiones teóricas sobre cuestiones prácticas (patrones de diseño, etc.).

Realizaremos programas a diario y, si no podemos, organicemos nuestro tiempo para poder dedicarle a programar unas horas por semana. Como en primer curso (en el que también habéis trabajado con Kodlin) trabajaremos principalmente con el lenguaje de programación Java, para mí, el lenguaje más completo, flexible y útil desde un punto de vista didáctico, aunque veremos **también ejemplos y ejercicios con Kotlin, especialmente para acceso a datos desde Android:

A lo largo del curso veremos tecnologías de acceso a datos como:

En definitiva, tecnologías actuales y demandadas en el mercado laboral para acceso a la información.

Consultad la información que aparece en el curso de tutoría de DAM:

https://mestre.iessanclemente.net/

Durante estos primeros días, haremos un pequeño repaso de Java y empezaremos la primera unidad, para que poco a poco os vayáis familiarizando con el entorno y la plataforma, es importante que actualicéis el perfil, y leáis esta guía completa (lo estáis haciendo), así como los documentos legales que aparecen en el curso de tutoría. Poned una foto real (perdón por no haberlo hecho, o casi).

Os adelanto, sin intención de dar miedo, al contrario, con ánimo de prevenir y que trabajéis desde ahora, porque, aunque con los conocimientos previos de programación os resultará sencillo, requiere práctica constante (sobre todo en clase) y enfrentarse a la materia con buen ánimo.

Unidades didácticas

El curso de Acceso a Datos de DAM consta de varias unidades didácticas. Cuando se comparta la Programación del Curso podréis ver los detalles de cada una de ellas. Por el momento se trata sólo de un primer borrador, que será completado al detalle en cada unidad didáctica.

A modo de adelanto, se indica la temporización aproximada de dichas unidades, teniendo en cuenta que son 9 sesiones por semana.

Primera evaluación

Se impartirán las dos primeras unidades y parte de la tercera unidad de herramientas ORM.

UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML. (UD. 1)

En esta unidad estudiaremos:

Gestión de información almacenada en ficheros, flujos, haciendo especial hincapié en los formatos JSON y algo de XML mediante aplicaciones informáticas escritas en Java.

a) Gestión de flujos, ficheros secuenciales, Acceso Directo y Directorios: desarrollo de aplicaciones que gestionan información almacenada en ficheros secuenciales, de acceso directo y en el sistema de directorios. En ella se aprenderá a identificar y utilizar las clases específicas para operar con cada tipo de fichero y con el sistema de directorios y a manejar las excepciones para el tratamiento de los posibles errores.

b) Gestión de ficheros JSON y, en menor medida, XML: desarrollo de aplicaciones que gestionan información almacenada en ficheros JSON:

  • con biblioteca Gson.
  • una introducción a Moshi.

También veremos algo de XML, y prenderemos a utilizar los procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos y a manejar las excepciones para el tratamiento de los posibles errores.

UD 2. Acceso a BD locales y remotas relacionales. Creación de una interfaz web sencilla (Vaadin y/o Thymeleaf)

Gestión de información almacenada en bases de datos relacionales por medio de aplicaciones informáticas escritas en Java con JDBC.

Para facilitar el trabajo de aplicaciones sencillas, existen muchos SGBD relacionales orientados a archivo (embebidos) opensource como H2, SQLite, HSQL, tinySQL, smallSQL o comerciales:

Pondremos especial interés en PostgreSQL, SQLite y H2, que son los más empleados en aplicaciones de escritorio y móviles.

Veremos patrones de diseño DAO y DTO, y cómo realizar operaciones CRUD (Create, Read, Update, Delete) sobre la base de datos.

Creación de una interfaz web sencilla, reflejado en el Proyecto curricular de Centro, probablemente con el framework Vaadin https://vaadin.com/ o Thymeleaf https://www.thymeleaf.org/, que veremos también con Spring Boot.

Aplicaciones que gestionan información almacenada en bases de datos relacionales. En ella se aprenderá a establecer conexiones con gestores de bases de datos relacionales embebidos e independientes utilizando conectores, a realizar operaciones de descripción, consulta y modificación de los datos contenidos en la base de datos, la extracción de los datos para realizar de forma adecuada las operaciones anteriores, a gestionar las transacciones y a manejar las excepciones para el tratamiento de los posibles errores

UD 3. Herramientas de mapeo objeto-relacional (ORM).

Parte de esta unidad se realizará en la segunda evaluación

Desarrollo de aplicaciones que gestionan información almacenada en base de datos relacionales utilizando herramientas mapeo objeto relacional (ORM), con JPA sobre Hibernate/EclipseLink y Spring Data, que traduzca la lógica de los objetos a la lógica relacional para su manipulación más sencilla.

En ella se aprenderá a instalar y configurar la herramienta ORM, a definir los ficheros de mapeo o, mejor, mediante anotaciones y las clases persistentes, a realizar el mapeo objeto relacional, a establecer sesiones, a cargar, almacenar y modificar los objetos persistentes, a realizar consultas en el lenguaje JPQL y en el lenguaje propio de la herramienta ORM.

También veremos Spring Boot y Spring Data, que facilitan el acceso a datos en aplicaciones Java, y que se integran perfectamente con JPA y Hibernate.

Segunda evaluación

En esta segunda evaluación se completará la tercera unidad y se impartirán las siguientes unidades. La unidad de Herramientas de mapeo objeto-relacional (ORM) se completará en esta evaluación, mientras que la unidad de creación de componentes se impartirá a lo largo del curso.

UD 4. Bases de datos no SQL. MongoDB

Características de las Bases de datos NoSQL.

Manejo de la información en bases de datos NoSQL.

Creación de aplicaciones informáticas que acceden a bases de datos NoSQL.

En ella se aprenderá a manejar y establecer conexiones con el gestor de bases de datos NoSQL, a realizar consultas y modificaciones de los datos contenidos en la base de datos. Para ello emplearemos dos estrategias: la API de bajo nivel y la API de alto nivel:

  • API nativa de MongoDB.
  • API de alto nivel de Spring Data MongoDB.

Ejemplos típicos de bases de datos NoSQL incluyen:

UD 5. Bases de datos nativas XML. Bases de datos orientadas a objeto. BD objeto-relacionales

Aplicaciones que gestionan la información almacenada en bases de datos nativas XML, como BaseX. En ella se aprenderá a manejar y establecer conexiones con el gestor de bases de datos nativas XML, a realizar consultas utilizado los lenguajes XPath y Xquery, o a modificar y eliminar los documentos XML.

Aplicaciones que gestionan información almacenada en bases de datos objeto-relacionales y orientadas a objetos. En ella se aprenderá a manejar gestores de base de datos que extienden las bases de datos relacionales añadiendo conceptos del modelo orientado a objetos y gestores de base de datos que almacenen los datos como objetos, estableciendo conexiones con estos tipos de gestores y realizando operaciones de almacenamiento, modificación y consulta de los objetos persistentes.

UD 6. Programación de componentes de acceso a datos

Programación de componentes de acceso a datos se irá estudiando a lo largo del curso, pues dichos componentes son la base de las herramientas ORM y de acceso a datos que hemos visto en las unidades anteriores.

En ella se aprenderá las bases de la programación orientada a componentes para las construcciones de aplicaciones basadas en el ensamblado de módulos reutilizables y se programarán componentes para el acceso los datos contenidos en diferentes sistemas de persistencia de datos utilizando herramientas de desarrollo de componentes. Materias como Diseño de Interfaces y Programación Multimedia y Dispositivos Móviles, así como la de Entornos de Desarrollo, serán fundamentales para completar esta unidad.

Parciales y evaluación

En cada evaluación se realizará un único examen parcial, aunque podría realizarse algún control después de cada unidad temática para realizar un seguimiento y una evaluación independiente de cada unidad. El examen parcial debe superarse para aprobar la evaluación correspondiente (nota mayor que 5).

Las pruebas o exámenes serán eminentemente prácticos y no se podrá disponer de material de ayuda, salvo el que el profesor o profesora considere necesario.

Cada unidad se evaluará de modo independiente, siendo necesario superar todas las unidades para aprobar la materia.

Se contempla la realización de prácticas y trabajos, al menos dos durante el curso, pero una por cada unidad, que se valorarán con un máximo de 1,5 puntos sobre la nota final.

Podrían plantearse trabajos o prácticas no obligatorias, que podrían emplearse para subir nota en caso de que fuese necesario y así lo considerase el profesor.

La nota de la evaluación está formada por el resultado del examen, las tareas obligatorias y un mínimo, referido a la participación y actitud durante el curso, con pesos de **85%, 10-15% y 0-5%, respectivamente**, Si durante la evaluación no se han realizado trabajos obligatorios el examen tendría un peso del porcentaje asociado a dicha nota. Una falta grave podría significar la suspensión de dicha evaluación.

El/los examen/es presencial de cada evaluación (2 en total) tendrá un valor de entre 8,5 y 10 puntos sobre la nota, dependiendo de si se ha realizado o no una tarea obligatoria. La puntuación restante se corresponde a la valoración de las tareas de la evaluación concreta.

Es imprescindible obtener un 5 sobre 10 en el examen para poder hacer media y aprobar. Aun así, es preciso que la media total, contando las tareas, sea igual o superior a un 5. Aunque las notas tendrán decimales, en el boletín de la evaluación se redondearán a valores enteros más próximos a dicha nota, entre 1 y 10.

Los/as estudiantes que hayan superado las evaluaciones parciales habrán aprobado la asignatura y no se presentarán al examen final.

La nota final será la media de todas las evaluaciones, siempre que se superen las dos evaluaciones.

Aunque la evaluación de cada unidad es independiente, cada unidad podrá incluir conceptos básicos necesarios de unidades previas (flujos, ficheros, etc.)

Examen final

Aquellas personas que hayan suspendido alguna de las dos evaluaciones deberán realizar un examen final con las partes pendientes, en principio toda la evaluación suspensa, aunque podría realizarse de una unidad concreta si así se considera.

El examen final constará de varios apartados, uno por cada unidad, de carácter eminentemente práctico que contenga la materia estudiada en cada una de las unidades. Si se supera, si se obtiene más de un 5, se aprobará la materia.

Se mantendrán las notas de las prácticas obligatorias entregadas a la hora de hacer la media del curso, que sí contabilizan en la nota final con el mismo peso que en las evaluaciones, además de las notas de las evaluaciones aprobadas. Se calculará como media de las notas de cada evaluación con decimales, para ser redondeada a la hora de poner la nota final, aproximada al entero más próximo a la nota media.

Los ejercicios de cada unidad que sean autoevaluables o boletines de clase no cuentan para la nota, aun así, podrían ser tenidos en cuenta (no la nota, sí el hecho de participar), junto con la participación, actitud, etc. para decantar alguna nota que pueda no ajustarse a los baremos estrictamente legales y establecidos.

Como he dicho, ¡mucho ánimo!, estoy convencido de que los resultados van a ser muy positivos, sólo requiere constancia y trabajo constante. A programar se aprende programando, como con cualquier otra actividad que requiera destreza, como tocar un instrumento.


Referencias

Última actualización: 23.09.2025

Subsecciones de Acceso a datos

UD 0. Refuerzo y ayudas complementarias

UD 0. Java General. Refuerzo y ayudas complementarias

Esta unidad es una unidad en la que se incluirá materia de ayuda y adicional que se considere necesario.

Última actualización: 23.09.2025

Subsecciones de UD 0. Refuerzo y ayudas complementarias

01 Configuración y despliegue

Manuales de configuración de Java, despligue, etc

En este apartado se muestran ejemplos y manuales para la ejecución, configuración y despligue en diferentes entornos de aplicaciones Java.

También podréis consultar el uso de herramientas del API de Java que consideremos interesantes.

Última actualización: 23.09.2025

Subsecciones de 01 Configuración y despliegue

01 Archivos Jar


1. Formato de Archivo Java™ (JAR)

  • El formato de archivo Java™ Archive (JAR) te permite agrupar múltiples archivos en un solo archivo de archivo.
  • Un archivo JAR contiene los archivos de clase y los recursos auxiliares asociados aplicaciones.

Beneficios:

Seguridad: se puede firmar digitalmente el contenido de un archivo JAR.

Tiempo de descarga reducido: cuando se trabaja con descarga de Internet pueden descargarse a un navegador en una sola transacción HTTP sin la necesidad de abrir una nueva conexión para cada archivo.

Formato comprimido: es un formato comprimido disminuyendo el espacio para su distribución y despliegue.

Empaquetado para bibliotecas Java: se pueden agregar bibliotecas, así como crear tus propias bibliotecas para distribuilas.

Sellado de paquetes: puede sellarse un paquete dentro de un archivo JAR para que todas las clases definidas en ese paquete estén dentro del archivo JAR.

Versionado de paquetes: puede contener datos sobre los archivos que contiene, como información de proveedor y versión.

Portabilidad: es una parte estándar de la API principal de la plataforma Java.

1. Sintaxis Archivos JAR

  • Los archivos JAR están empaquetados con el formato de archivo ZIP.

  • Para realizar tareas básicas con archivos JAR se emplea la herramienta jar del JDK, que se invoca mediante el comando jar.

Sintaxis general:

jar {ctxui}[vfm0Me] [jar-file] [manifest-file] [entry-point] [-C dir] files ...

Operaciones principales con Archivos JAR:

Operación Comando
Crear un archivo JAR jar cf jar-file input-file(s)
Ver el contenido de un archivo JAR jar tf jar-file
Extraer el contenido de un archivo JAR jar xf jar-file
Extraer archivos específicos de un JAR jar xf jar-file archived-file(s)
Ejecutar una aplicación empaquetada como JAR (requiere el encabezado en el archivo Manifiest con Main-Class) java -jar app.jar
Invocar un applet empaquetado como JAR (desprobado) <applet code=AppletClassName.class archive="JarFileName.jar" width=width height=height></applet>

2. Creación de un JAR

El formato básico del comando para crear un archivo JAR es:

jar cf nombre-archivo-jar archivos(s)

Las opciones y argumentos utilizados en este comando son:

  • La opción c indica se cree un archivo JAR.
  • La opción f indica que la salida debe ir a un archivo en lugar de a stdout (salida por pantalla).
  • nombre-archivo-jar es el nombre del archivo JAR resultante. Puede usarse cualquier nombre de archivo para un archivo JAR. Por convenio, los nombres de archivo JAR se les da una extensión .jar, aunque esto no es obligatorio.
  • El argumento archivos(s) es una lista separada por espacios de uno o más archivos que deseas incluir en tu archivo JAR. El argumento archivo(s) puede contener el símbolo de comodín *. Si alguno de los “archivos de entrada” son directorios, el contenido de esos directorios se agrega al archivo JAR de forma recursiva.
  • Las opciones c y f pueden aparecer en cualquier orden, pero no debe haber ningún espacio entre ellas.

Este comando generará un archivo JAR comprimido y lo colocará en el directorio actual.
El comando también generará un archivo MANIFEST.MF predeterminado para el archivo JAR.

El archivo de META-INF/MANIFEST.MF predeterminado contiene lo siguiente (depende de la versión de Java):

Manifest-Version: 1.0
Created-By: 1.8.0_181 (Oracle Corporation)

Si se le indica el nombre de la clase principal, el archivo MANIFEST.MF predeterminado se actualizará para contener la entrada Main-Class:

Main-Class: nombreClasePrincipal

Nota: Los metadatos en el archivo JAR, como los nombres de entrada, comentarios y contenido del manifiesto, deben codificarse en UTF8.

Puedes agregar cualquiera de estas opciones adicionales a las opciones cf del comando básico:

Opciones de la orden jar

Opción Descripción
v Produce una salida detallada (verbose) en la creación del JAR (nombre de cada archivo a medida que se añade al archivo JAR).
0 (cero) Indica que el archivo JAR NO se comprima.
M Indica que no se debe generar el archivo de manifiesto predeterminado (MANIFEST.MF).
m incluir información del manifiesto desde un archivo de manifiesto existente: jar cmf jar-file manifest-existente input-file(s).
-C Para cambiar de directorio durante la ejecución del comando.

Advertencia: El MANIFEST.MF debe terminar con una nueva línea o retorno de carro.
La última línea no se analizará correctamente si no termina con una nueva línea o retorno de carro.

3. Visualización del contenido de un JAR

El formato básico de la orden para ver el contenido de un archivo JAR es:

jar tf jar-file
  • La opción t indica que deseas ver la tabla de contenidos del archivo JAR.
  • La opción f indica que el archivo JAR cuyo contenido se va a ver.
  • El argumento jar-file es la ruta y el nombre del archivo JAR cuyo contenido deseas ver.
  • Las opciones t y f pueden aparecer en cualquier orden, pero no debe haber ningún espacio entre ellas.

Este comando mostrará la tabla de contenidos del archivo JAR por pantalla.

Se puede añadir la opción detallada, v, para obtener información adicional sobre tamaños de archivo y fechas de última modificación en la salida.

4. Archivos JAR como Aplicaciones

Es posible ejecutar aplicaciones empaquetadas en archivos JAR con java:

java -jar archivo-jar
  • Solo se puede indicar un archivo JAR, que debe contener todo el código específico de la aplicación.

  • Lo más importante es que el archivo Jar debe tener información de la clase con el main, que es el punto de entrada de la aplicación.

  • Para indicar qué clase es el punto de entrada de la aplicación, debe añadirse Main-Class al manifiesto del archivo JAR.

  • El encabezado tiene la forma:

Main-Class: paquete.ClasePrincipal

El valor del encabezado, paquete.ClasePrincipal, es el nombre de la clase que es el punto de entrada de la aplicación.

Nota: El atributo Main-Class es opcional en el manifiesto de un archivo JAR. Si no se especifica, el archivo JAR no se puede ejecutar directamente desde la línea de comandos.

Cuando se indica Main-Class en el archivo MANIFEST.MF, puedes ejecutar la aplicación desde la línea de órdenes:

java -jar app.jar

Para ejecutar la aplicación desde el archivo JAR que está en otro directorio, debes especificar la ruta de ese directorio:

java -jar path/app.jar
(Harry Haller) Última actualización: 23.09.2025

02 Creación de un JAR ejecutable con Java con Apache Maven


1. Introducción

Este documento explicaremos los distintos modos de empaquetar un proyecto Maven en un archivo Jar ejecutable.

Cuando creamos un archivo jar, generalmente queremos ejecutarlo fácilmente, sin utilizar el IDE. Muchas veces nos vemos sorprendidos al comprobar que el archivo de manifiesto no incorpora referencias al main o no so incluyen las bibliotecas.

Veremos varias configuraciones y pros/contras de cada uno de estos enfoques para crear un JAR ejecutable.

En este artículo, describimos muchas formas de crear un jar ejecutable con varios complementos de Maven.

2. Configuración manual

Este modo de hacerlo nos da flexiblidad, pues sólo se requiere de un proyecto maven y añadir los elementos necesarios al fichero de configuración de maven, pom.xml.

No necesitamos ninguna dependencia adicional para crear un archivo jar ejecutable. sólo necesitamos crear un proyecto Java Maven y tener al menos una clase con el método main(…), la entrada al programa.

En nuestro ejemplo, creamos una clase Java llamada AppExemplo.

También debemos asegurarnos de que nuestro pom.xml contenga estos elementos:

<modelVersion>4.0.0</modelVersion>
<groupId>com.javhoz</groupId>
<artifactId>core-java</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>

Lo más importante aquí es el tipo de empaquetamiento, para crear un jar ejecutable el packaging tipo de jar.

Ahora podemos comenzar a usar las diversas soluciones.

2.1. Configuración del archivo pom.xml

Añadir bibliotecas con las dependencias

Comencemos con un enfoque manual con la ayuda del maven-dependency-plugin y copy-dependencies, que copia las dependencias del proyecto desde el repositorio a una ubicación definida, en este caso el directorio libs:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>
                    ${project.build.directory}/libs
                </outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Hay dos aspectos importantes a tener en cuenta.

Primero, especificamos la meta copy-dependencies, que le dice a Maven que copie estas dependencias en el directorio de salida especificado. En nuestro caso, crearemos una carpeta llamada libs dentro del directorio de despliegue del proyecto (que suele llamarse target).

Creación de un jar ejecutable

En segundo lugar, vamos a crear un jar ejecutable indicándoles la ruta a las biblitecas anteriores en el classpath, dentro del plugin que permite crear jar :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>libs/</classpathPrefix>
                <mainClass>
                    com.javhoz.executable.AppExemplo
                </mainClass>
            </manifest>
        </archive>
    </configuration>
</plugin>

La parte más importante de esto es la configuración del archivo que se produce dentro del archivo MANIFEST.MF del JAR https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html.

Con esta configuración de Maven se añaden las rutas a las bibliotecas del proyecto dentro de la carpeta libs/ y se indica qué clase tiene el main.

Consejo

El nombre de la clase con el main tiene que estar completamente calificado, incluytendo el nombre del paquete.

Las ventajas y desventajas de este enfoque son:

  • Ventajas: Proceso transparente, donde podemos especificar cada paso
  • Inconvenientes: se trata de un proceso manual, en el que las dependencias están fuera del jar final, lo que significa que nuestro jar ejecutable únicamente se ejecutará si la carpeta libs es accesible y visible para un jar.

2.2. Incorporación de las dependencias dentro del JAR: maven-assembly-plugin

El Plugin de ensamblado de Apache Maven maven-assembly-plugin permite agregar al paquete de salida del proyecto (jar en este caso), módulos, documentación del sitio y otros archivos en un único paquete ejecutable ("…permite a los desarrolladores combinar los resultados del proyecto en un único archivo distribuible que también contiene dependencias, módulos, documentación del sitio y otros archivos").

El objetivo principal (y ahora único) en el plugin de ensamblaje es el crear un archivo único, que se utiliza para crear todos los ensamblajes.

Puedes consultar el uso de este plugin en la página oficial de Apeche Maven

La configuración del pom.xml debe ser algi similar a lo siguiente:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
            <configuration>
                <archive>
                <manifest>
                    <mainClass>
                        com.javhoz.executable.AppExemplo
                    </mainClass>
                </manifest>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </execution>
    </executions>
</plugin>
Clase con el main

Del mismo modo que añadiendo las bibliotecas a un directorio lib, De manera similar al enfoque manual, necesitamos indicar el nombre de la clase con el main. La diferencia es que el Plugin de ensamblaje de Maven copiará automáticamente todas las dependencias necesarias dentro del mismo archivo jar.

En la parte descriptorRefs del código de configuración, se indica el nombre que se agregará al nombre del proyecto (puede cambiarse)

La salida en nuestro ejemplo se llamará core-java-jar-with-dependencies.jar: “Nota: … Tenga en cuenta que el complemento de ensamblaje le permite especificar varios descriptorRefsa la vez para producir múltiples tipos de ensamblajes en una sola invocación.

  • Ventajas: las dependencias se añaden dentro de un único archivo jar, dándole portabilidad “total”.
  • Desventajas: no podemos reubicar las calses del proyecto. A veces, el tamaño puede aumentar considerablemente y sólo precisamos distribuir nuestras clases.

2.3. Plugin Maven Shade: maven-shade-plugin

El plugin de sombreado de Apache Maven proporciona la capacidad de empaquetar el artefacto en un uber-jar, “..proporciona la capacidad de empaquetar el artefacto en un uber-jar, incluidas sus dependencias y sombrear (es decir, cambiar el nombre) los paquetes de algunas de las dependencias.

Esto es, una vez creado el JAR permite cambiar las dependencias si se precisa en algún momento (versiones, etc.). Uso del plugin: https://maven.apache.org/plugins/maven-shade-plugin/usage.html

Configuración del pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <shadedArtifactAttached>true</shadedArtifactAttached>
                <transformers>
                    <transformer implementation=
                      "org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.javhoz.executable.AppExemplo</mainClass>
                </transformer>
            </transformers>
        </configuration>
        </execution>
    </executions>
</plugin>

El archivo de configuración tiene 3 partes principales:

  • Necesitamos especificar la clase principal de la aplicación: com.javhoz.executable.AppExemplo
  • <shadedArtifactAttached> indicas las dependencias que deben ser empaquetadas en el jar.
  • Debe indicarse la implementación del transformador, transformer implementation=, que en el ejemplo se emplea el estándar que añade las entradas al archiv MANIFEST.
Implementaciones del transformer del plugin

Transformadores del plugin de org.apache.maven.plugins.shade.resource son:

Transformer Descripción
ApacheLicenseResourceTransformer Evita la duplicación de licencias
ApacheNoticeResourceTransformer Prepara el NOTICE combinado
AppendingTransformer Agrega contenido a un recurso
ComponentsXmlResourceTransformer Agrega el archivo components.xml de Plexus
DontIncludeResourceTransformer Evita la inclusión de recursos coincidentes
IncludeResourceTransformer Agrega archivos del proyecto
ManifestResourceTransformer Establece entradas en el MANIFEST
ServicesResourceTransformer Fusiona los recursos META-INF/services
XmlAppendingTransformer Agrega contenido XML a un recurso XML

El archivo de salida se llamará core-java-0.1.0-SNAPSHOT-shaded.jar, donde core-java es el nombre de nuestro proyecto seguido por la versión de snapshot y el nombre del plugin.

  • Ventajas: dependencias dentro del archivo jar, control avanzado del empaquetado del proyecto, con sombreado y reubicación de clases.
  • Deventajas: configuración compleja (especialmente si queremos usar funciones avanzadas).

2.4. Plugin One Jar Maven: onejar-maven-plugin

Otra opción, poco recomendable (como curiosidad), menos interesante y comercial para crear un jar ejecutable es el proyecto One Jar, que proporciona un loader de clases personalizado que sabe cómo cargar clases y recursos desde archivos jar dentro de un archivo, en lugar de desde archivos jar en el sistema de archivos. Requiere dependencias.

Configuración del pom.xml:

<plugin>
    <groupId>com.jolira</groupId>
    <artifactId>onejar-maven-plugin</artifactId>
    <executions>
        <execution>
            <configuration>
                <mainClass>com.javhoz.executable.AppExemplo</mainClass>
                <attachToBuild>true</attachToBuild>
                <filename>
                  ${project.build.finalName}.${project.packaging}
                </filename>
            </configuration>
            <goals>
                <goal>one-jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • Se debe especificar la clase principal y adjuntar todas las dependencias a la construcción, utilizando attachToBuild = true.
  • Se debe proporcionar el nombre de archivo de salida.
  • El objetivo para Maven es one-jar.

One Jar es una solución comercial que hará que las dependencias no se expandan en el sistema de archivos en tiempo de ejecución.

  • Ventajas: modelo de delegación limpio, permite que las clases estén en el nivel superior de One Jar, admite archivos jar externos y puede admitir bibliotecas nativas
  • Desventajas:: no es compatible desde 2012

2.5. Plugin Spring Boot Maven: spring-boot-maven-plugin

Otr opción interesante es el Plugin Spring Boot Maven, que permite empaquetar archivos jar o war ejecutables y ejecutar una aplicación “in situ”.

Para usarlo, necesitamos usar al menos la versión 3.2 de Maven. La descripción detallada está disponible esta disponible en la página de Spring.

Configuración del pom.xml:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
            <configuration>
                <classifier>spring-boot</classifier>
                <mainClass>
                  com.javhoz.executable.AppExemplo
                </mainClass>
            </configuration>
        </execution>
    </executions>
</plugin>

Hay dos diferencias entre el plugin Spring y los demás:

El objetivo (goal) de la ejecución se llama repackage, y el clasificador (classifier) se llama spring-boot.

NO NECESITAMOS tener una aplicación Spring Boot para usar este plugin.

  • Ventajas: dependencias dentro de un archivo jar, podemos ejecutarlo en cualquier ubicación accesible, control avanzado del empaquetado del proyecto, excluyendo dependencias del archivo jar, etc., empaquetado de otros tipos de archivo como war
  • Desventajas: añade clases innecesarias de Spring y Spring Boot, pues no requiere usar Spring Boot para emplear este plugin.

2.6. Aplicación Web ejecutable Tomcat: tomcat7-maven-plugin

Por último, si queremos hacer una aplicación web independiente que esté empaquetada dentro de un archivo jar.

Necesitamos usar un plugin diferente, diseñado para crear archivos jar ejecutables:

Configuración del pom.xml:

<plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <version>2.0</version>
    <executions>
        <execution>
            <id>tomcat-run</id>
            <goals>
                <goal>exec-war-only</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <path>/</path>
                <enableNaming>false</enableNaming>
                <finalName>webapp.jar</finalName>
                <charset>utf-8</charset>
            </configuration>
        </execution>
    </executions>
</plugin>

El goal está configurado como exec-war-only, la ruta al servidor se especifica dentro de la etiqueta de configuration, con propiedades adicionales, como finalName, charset, etc.

Para construir un jar, ejecutamos mvn package, lo que dará como resultado la creación de webapp.jar en el directorio target.

Para ejecutar la aplicación, simplemente escribimos java -jar target/webapp.jar en la consola y tratamos de probarlo especificando el localhost:8080/ en un navegador. Ya tenemos nuestra aplicación Web ejecutándose desde línea de órdenes un archivo JAR ;-)

  • Ventajas: tener un único archivo, fácil de implementar y ejecutar
  • Desventajas: el tamaño del archivo es mucho mayor, debido al empaquetado de la distribución integrada de Tomcat dentro de un archivo jar.
Implementaciones del transformer del plugin

Ten en cuenta que esta es la última versión de este plugin, que admite el servidor Tomcat7. Para evitar errores, podemos verificar que la dependencia para Servlets tenga el ámbito configurado como provided, de lo contrario, habría un conflicto en el tiempo de ejecución del jar ejecutable:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>
Última actualización: 23.09.2025

03. Dependencias Maven.

1. Dependencias Maven.

2. Logging

SLF4J (The Simple Logging Facade for Java) es una fachada o interfaz para varios sistemas de registro de eventos (logging) en Java. Permite a los desarrolladores cambiar de sistema de registro de eventos en tiempo de ejecución sin tener que modificar el código fuente. Para más información, visita la página oficial de SLF4J: http://www.slf4j.org/

https://mvnrepository.com/artifact/org.slf4j/slf4j-api https://central.sonatype.com/artifact/org.slf4j/slf4j-api

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.16</version>
</dependency>

Además, precisamos alguna implementación de SLF4J. En este caso vamos a usar Logback:

https://mvnrepository.com/artifact/ch.qos.logback/logback-classic https://central.sonatype.com/artifact/ch.qos.logback/logback-classic

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.16</version>
</dependency>

2. Json

2.1 Gson

Gson es una biblioteca Java que se utiliza para convertir objetos Java en su representación JSON. También puede ser utilizado para convertir una cadena JSON en un objeto Java equivalente. Gson es una biblioteca de código abierto desarrollada por Google. Puedes encontrar más información en la página oficial de Gson: https://github.com/google/gson

https://mvnrepository.com/artifact/com.google.code.gson/gson https://central.sonatype.com/artifact/com.google.code.gson/gson

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>

2.2. Jackson Databind

Jackson es una biblioteca Java de código abierto para convertir objetos Java en su representación JSON y viceversa. Jackson es una de las bibliotecas de serialización y deserialización JSON más populares en Java. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.2</version>
</dependency>

2.3. Jackson Core

Jackson Core es una biblioteca Java de código abierto para procesar JSON (Stream API). Jackson Core proporciona las clases básicas para trabajar con JSON, como JsonNode, JsonParser y JsonGenerator. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson-core

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-core

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.18.2</version>
</dependency>

3. JUnit

JUnit es un framework open-source que se utiliza para realizar pruebas unitarias en Java. JUnit es una herramienta importante en el desarrollo de software, ya que permite a los desarrolladores probar su código de manera eficiente y asegurarse de que funciona correctamente. Puedes encontrar más información en la página oficial de JUnit: https://junit.org/junit5/

https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api https://central.sonatype.com/artifact/org.junit.jupiter/junit-jupiter-api

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.11.4</version>
    <scope>test</scope>
</dependency>

Ejemplo de uso:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

public class MyTest {

    @Test
    public void test() {
        assertEquals(2, 1 + 1);
    }
}

4. Drivers JDBC

Para trabajar con bases de datos, necesitamos los drivers JDBC correspondientes.

4.1. H2

H2 es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor. Además, admite transacciones, encriptación, funciones de usuario o procedimientos almacenados. Además, puede almacenarse en memoria o en disco.

Puedes encontrar más información en la página oficial de H2: http://www.h2database.com/

https://mvnrepository.com/artifact/com.h2database/h2 https://central.sonatype.com/artifact/com.h2database/h2

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.3.232</version>
</dependency>

Es importante hacer notar que las incompatibilidades entre versiones diferentes de H2, por lo que se recomienda tener control sobre qué versión se está utilizando.

URL: jdbc:h2:mem:testdb (base de datos en memoria) Driver: org.h2.Driver URL (fichero): jdbc:h2:rutaALaBaseDatos;DATABASE_TO_UPPER=false (base de datos en fichero)

El Driver JDBC para H2 hace la conversión automática de los nombres de las tablas y columnas a mayúsculas, por lo que si queremos conservar los nombres originales, debemos añadir DATABASE_TO_UPPER=false a la URL de conexión.

4.2. SQLite JDBC Driver

SQLite es una base de datos relacional embebida, que no requiere un servidor. Es muy ligera y rápida, y se puede utilizar en aplicaciones de escritorio, móviles o en la web. Puedes encontrar más información en la página oficial de SQLite: https://www.sqlite.org/index.html

Existen varias implementaciones de SQLite en Java, pero vamos a usar Xerial SQLite JDBC Driver:

https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc https://central.sonatype.com/artifact/org.xerial/sqlite-jdbc

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.48.0.0</version>
</dependency>

URL: jdbc:sqlite:rutaALaBaseDatos (base de datos en fichero) Driver: org.sqlite.JDBC

Existen otras API para SQLite, como las versiones originales de androidx: https://developer.android.com/jetpack/androidx/releases/sqlite, pero dicha versión no es compatible con Java SE y se usaba antiguamente para android, antes de la aparicion de Room.

4.3. PostgreSQL JDBC Driver

PostgreSQL es un sistema de gestión de bases de datos relacional de código abierto y muy potente. Puedes encontrar más información en la página oficial de PostgreSQL: https://www.postgresql.org/

https://mvnrepository.com/artifact/org.postgresql/postgresql https://central.sonatype.com/artifact/org.postgresql/postgresql

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.5</version>
</dependency>

URL: jdbc:postgresql://localhost:5432/nombredelabasededatos Driver: org.postgresql.Driver

El usuario y la contraseña se pasarán como parámetros en la URL de conexión:

    String url = "jdbc:postgresql://localhost:5432/nombredelabasededatos";
    String user = "usuario";
    String password = "contraseña";
    Connection conn = DriverManager.getConnection(url, user, password);

Si queremos añadirlos a la URL:

    String url = "jdbc:postgresql://localhost:5432/nombredelabasededatos?user=usuario&password=contraseña";
    Connection conn = DriverManager.getConnection(url);

4.4. MySQL Connector/J

MySQL Connector/J es un controlador JDBC Tipo 4, lo que significa que es una implementación Java pura del protocolo MySQL y no depende de las bibliotecas de cliente MySQL. Como los anteriores, este controlador admite el registro automático con DriverMaganer, lo que significa que no es necesario cargar explícitamente el controlador.

https://mvnrepository.com/artifact/com.mysql/mysql-connector-j https://central.sonatype.com/artifact/com.mysql/mysql-connector-j

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.1.0</version>
</dependency>

URL: jdbc:mysql://localhost:3306/nombredelabasededatos Driver: com.mysql.cj.jdbc.Driver

La URL puede recoger parámetros, como:

    String url = "jdbc:mysql://localhost:3306/nombredelabasededatos?user=usuario&password=contraseña";
    Connection conn = DriverManager.getConnection(url);

4.5. HyperSQL Database (HSQLDB)

HSQLDB es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor: https://hsqldb.org/

https://mvnrepository.com/artifact/org.hsqldb/hsqldb https://central.sonatype.com/artifact/org.hsqldb/hsqldb

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.7.4</version>
</dependency>

URL: jdbc:hsqldb:mem:testdb (base de datos en memoria) Driver: org.hsqldb.jdbc.JDBCDriver URL para servidor: jdbc:hsqldb:hsql://localhost/testdb URL para fichero: jdbc:hsqldb:file:nombrebasededatos

5. Dependencias para JPA

5.1. Jakarta Persistence API (JPA)

La Java Persistence API (JPA) es una especificación de Java que describe la gestión de la persistencia de los objetos en las aplicaciones Java. JPA define un conjunto de interfaces y anotaciones que permiten a los desarrolladores mapear objetos Java a tablas de bases de datos y viceversa. Puedes encontrar más información en la página oficial de JPA:

https://mvnrepository.com/artifact/jakarta.persistence/jakarta.persistence-api https://central.sonatype.com/artifact/jakarta.persistence/jakarta.persistence-api

JPA 3.1:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
</dependency>

JPA 3.2:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
</dependency>

5.2. Hibernate

Hibernate es un framework de mapeo objeto-relacional (ORM) para Java. Hibernate simplifica el desarrollo de aplicaciones Java que interactúan con bases de datos relacionales. Puedes encontrar más información en la página oficial de Hibernate: https://hibernate.org/

https://mvnrepository.com/artifact/org.hibernate/hibernate-core https://central.sonatype.com/artifact/org.hibernate/hibernate-core

La versión compatible con JPA 3.1 es la versión 6:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.6.5.Final</version>
</dependency>

La versión compatible con JPA 3.2 es la versión 7, que todavía está en desarrollo:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.0.0.Beta3</version>
</dependency>

Pronto se lanzará la versión final de Hibernate 7, que será compatible con JPA 3.2. Esperamos.

EclipseLink es otro framework de mapeo objeto-relacional (ORM) para Java. EclipseLink es una implementación de la especificación JPA y proporciona una serie de características avanzadas, como el mapeo de herencia, el mapeo de tablas, el mapeo de relaciones y la consulta de objetos. Puedes encontrar más información en la página oficial de EclipseLink: https://www.eclipse.org/eclipselink/

https://mvnrepository.com/artifact/org.eclipse.persistence/eclipselink https://central.sonatype.com/artifact/org.eclipse.persistence/eclipselink

La versión compatible con JPA 3.1 es la versión 4:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>4.0.5</version>
</dependency>

La versión compatible con JPA 3.2 es la versión 5, que todavía está en desarrollo:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>5.0.0-B05</version>
</dependency>

Pronto se lanzará la versión final de EclipseLink 5, que será compatible con JPA 3.2. Esperamos.

6. Dependencias para Spring

6.1. Spring Core

Spring Core es el núcleo del framework Spring. Proporciona las funcionalidades básicas de Spring, como la inyección de dependencias y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring: https://spring.io/projects/spring-framework

https://mvnrepository.com/artifact/org.springframework/spring-core https://central.sonatype.com/artifact/org.springframework/spring-core

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>6.2.2</version>
</dependency>

6.2. Spring Boot

Spring Boot es un proyecto de Spring que simplifica el desarrollo de aplicaciones Java. Proporciona una serie de características, como la configuración automática, el embebido de servidores, la gestión de dependencias y la creación de aplicaciones ejecutables. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot https://central.sonatype.com/artifact/org.springframework.boot/spring-boot

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>3.4.1</version>
</dependency>

Spring Boot Starter es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>

6.3. Spring Data JPA

Spring Data JPA es un proyecto de Spring que simplifica el acceso a datos en aplicaciones Java. Proporciona una serie de características, como la creación de repositorios, la generación de consultas y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring Data JPA: https://spring.io/projects/spring-data-jpa

https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa https://central.sonatype.com/artifact/org.springframework.data/spring-data-jpa

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>3.4.2</version>
</dependency>

6.4. Spring Boot Starter Data JPA

Spring Boot Starter Data JPA es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot que utilizan Spring Data JPA. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.4.1</version>
</dependency>

7. Lenguajes sobre JVM

7.1. Kotlin

Kotlin es un lenguaje de programación moderno y conciso que se ejecuta sobre la JVM. Kotlin es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Kotlin y viceversa. Puedes encontrar más información en la página oficial de Kotlin: https://kotlinlang.org/

IDEs como IntelliJ IDEA o Android Studio soportan Kotlin de forma nativa, pero también puedes usar Kotlin en otros IDE añadiendo las dependencias necesarias.

https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib https://central.sonatype.com/artifact/org.jetbrains.kotlin/kotlin-stdlib

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>2.1.0</version>
</dependency>

7.2. Scala

Scala es un lenguaje de programación funcional y orientado a objetos que se ejecuta sobre la JVM. Scala es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Scala y viceversa. Puedes encontrar más información en la página oficial de Scala: https://www.scala-lang.org/

IDEs como IntelliJ IDEA o Eclipse soportan Scala de forma nativa, pero también puedes usar Scala en otros IDE añadiendo las dependencias necesarias.

https://mvnrepository.com/artifact/org.scala-lang/scala3-library_3 https://central.sonatype.com/artifact/org.scala-lang/scala3-library_3

<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala3-library_3</artifactId>
    <version>3.6.3</version>
</dependency>

Referencias

Última actualización: 23.09.2025

02 Java General

00.02 Apuntes de java en general y ayudas

Última actualización: 23.09.2025

Subsecciones de 02 Java General

00.01 Java Stream API


Introducción

La API de Streams de Java ofrece un enfoque funcional para el procesamiento de colecciones de objetos. Se introdujo en Java 8 junto con varias otras características de programación funcional. Este tutorial de Java Stream explicará cómo funcionan estos streams funcionales y cómo usarlos.

La API de Java Stream no está relacionada con Java InputStream y Java OutputStream de Java IO. InputStream y OutputStream se relacionan con flujos de bytes, mientras que la API de Stream de Java se utiliza para procesar flujos de objetos.

1. Definición de Java Stream

Un Stream en Java es un componente capaz de realizar una iteración interna de sus elementos, lo que significa que puede iterar sobre sus elementos por sí mismo. En contraste, al usar las características de iteración de Java Collections (por ejemplo, un Java Iterator o el bucle for-each de Java utilizado con un Java Iterable), debes implementar la iteración de los elementos tú mismo.

2. Procesamiento de Streams

Puedes adjuntar oyentes a un Stream. Estos oyentes se llaman cuando el Stream itera internamente los elementos. Los oyentes se llaman una vez por cada elemento en el stream. De esta manera, cada oyente procesa cada elemento en el stream. Esto se denomina procesamiento de stream.

Los oyentes de un stream forman una cadena. El primer oyente en la cadena puede procesar el elemento en el stream y luego devolver un nuevo elemento para que el siguiente oyente en la cadena lo procese. Un oyente puede devolver el mismo elemento o uno nuevo, dependiendo del propósito de ese oyente (procesador).

3. Crear un Stream

Hay muchas formas de obtener un Stream en Java. Una de las formas más comunes de obtener un Stream es desde una Java Collection. Un ejemplo de cómo obtener un Stream desde una Java List:

List<String> items = new ArrayList<String>();

items.add("uno");
items.add("dos");
items.add("tres");

Stream<String> stream = items.stream();    

Este ejemplo crea primero una lista de Java, luego agrega tres Java Strings y, finalmente, llama al método stream() para obtener una instancia de Stream.

4. Operaciones Terminales y No Terminales

La interfaz Stream tiene una selección de operaciones terminales y no terminales. Una operación no terminal de stream es una operación que agrega un oyente al stream sin hacer nada más. Una operación terminal de stream es una operación que inicia la iteración interna de los elementos, llama a todos los oyentes y devuelve un resultado.

Un ejemplo de Java Stream que contiene tanto una operación no terminal como una terminal:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class EjemplosDeStream {

    public static void main(String[] args) {
        List<String> listaDeCadenas = new
                ArrayList<>();

        listaDeCadenas.add("Uno");
        listaDeCadenas.add("Dos");
        listaDeCadenas.add("Tres");

        // Operación no terminal (filter)
        Stream<String> streamFiltrado = listaDeCadenas.stream().filter(s -> s.startsWith("T"));

        // Operación terminal (forEach)
        streamFiltrado.forEach(System.out::println);
    }
}

En este ejemplo, filter es una operación no terminal que filtra elementos basándose en el predicado proporcionado. Luego, forEach es una operación terminal que itera sobre los elementos restantes y aplica la función de impresión.

5. Operaciones No Terminales

Las operaciones no terminales de stream devuelven un nuevo Stream. Estas operaciones no realizan ninguna iteración interna. Se ejecutan “perezosamente”, lo que significa que no realizan ninguna acción hasta que se activa una operación terminal.

filter()

La operación filter es una operación no terminal que acepta un Java Predicate)como argumento y devuelve un nuevo Stream que contiene solo los elementos que cumplen con el predicado.

List<String> listaDeCadenas = Arrays.asList("Uno", "Dos", "Tres", "Cuatro", "Cinco");

// Filtrar elementos que comienzan con "C"
Stream<String> streamFiltrado = listaDeCadenas.stream().filter(s -> s.startsWith("C"));

// Imprimir elementos
streamFiltrado.forEach(System.out::println);

En este ejemplo, filter se utiliza para seleccionar solo las cadenas que comienzan con “C”.

map()

La operación map es una operación no termina que transforma cada elemento del Stream utilizando la función proporcionada como argumento. Devuelve un nuevo Stream que contiene los elementos transformados:

<R> Stream<R> map(Function<? super T,? extends R> mapper)
R apply(T t) // "método" de la interface Function
List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Convertir a mayúsculas
Stream<String> streamMayusculas = listaDeCadenas.stream().map(String::toUpperCase);

// Imprimir elementos
streamMayusculas.forEach(System.out::println);

En este ejemplo, map se utiliza para convertir cada cadena a mayúsculas.

flatMap()

La operación flatMap es una operación no terminal que transforma cada elemento del Stream en cero o más elementos según la función proporcionada como argumento. Luego, combina los elementos resultantes en un solo Stream.

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
R apply(T t)
List<List<Integer>> numeros = Arrays.asList(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9)
);

// Obtener un solo Stream de todos los números
Stream<Integer> numerosStream = numeros.stream().flatMap(List::stream);

// Imprimir elementos
numerosStream.forEach(System.out::println);

En este ejemplo, flatMap se utiliza para obtener un solo Stream de todos los números en las listas anidadas.

distinct()

La operación distinct es una operación no terminal que elimina los elementos duplicados del Stream, basándose en su implementación del método equals().

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange", "banana", "apple");

// Eliminar duplicados
Stream<String> streamSinDuplicados = listaDeCadenas.stream().distinct();

// Imprimir elementos
streamSinDuplicados.forEach(System.out::println);

En este ejemplo, distinct se utiliza para eliminar las cadenas duplicadas.

limit()

La operación limit es una operación no terminal que reduce la longitud del Stream a la cantidad especificada.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange", "grape");

// Limitar a los primeros dos elementos
Stream<String> streamLimitado = listaDeCadenas.stream().limit(2);

// Imprimir elementos
streamLimitado.forEach(System.out::println);

En este ejemplo, limit se utiliza para limitar el Stream a los primeros dos elementos.

peek()

La operación peek es una operación no terminal que permite realizar un side effect en cada elemento del Stream, como imprimir el elemento antes de que se pase a la siguiente operación.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Imprimir cada elemento antes de la transformación
List<String> resultado = listaDeCadenas.stream()
        .peek(System.out::println)
        .map(String::toUpperCase)
        .collect(Collectors.toList());

En este ejemplo, peek se utiliza para imprimir cada elemento antes de que se transforme a mayúsculas.

6. Operaciones Terminales

Las operaciones terminales de stream inician la iteración interna de los elementos y devuelven un resultado final. Después de que se realiza una operación terminal, un Stream no puede ser utilizado de nuevo.

anyMatch()

La operación anyMatch es una operación terminal que devuelve true si al menos un elemento del Stream cumple con la condición dada, de lo contrario, devuelve false.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Verificar si alguna cadena contiene "nan"
boolean contieneNan = listaDeCadenas.stream().anyMatch(s -> s.contains("nan"));

System.out.println(contieneNan);  // Salida: true

En este ejemplo, anyMatch se utiliza para verificar si alguna cadena contiene la subcadena “nan”.

allMatch()

La operación allMatch es una operación terminal que devuelve true si todos los elementos del Stream cumplen con la condición dada, de lo contrario, devuelve false.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Verificar si todas las cadenas tienen longitud mayor que 3
boolean todasLargas = listaDeCadenas.stream().allMatch(s -> s.length() > 3);

System.out.println(todasLargas);  // Salida: true

En este ejemplo, allMatch se utiliza para verificar si todas las cadenas tienen una longitud mayor que 3.

noneMatch()

La operación noneMatch es una operación terminal que devuelve true si ninguno de los elementos del Stream cumple con la condición dada, de lo contrario, devuelve false.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Verificar si ninguna cadena contiene "grape"
boolean ningunaContieneUva = listaDeCadenas.stream().noneMatch(s -> s.contains("gr

ape"));

System.out.println(ningunaContieneUva);  // Salida: true

En este ejemplo, noneMatch se utiliza para verificar si ninguna cadena contiene la subcadena “grape”.

findFirst()

La operación findFirst es una operación terminal que devuelve el primer elemento del Stream en un Optional. Si el Stream está vacío, devuelve un Optional vacío.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Obtener el primer elemento
Optional<String> primerElemento = listaDeCadenas.stream().findFirst();

primerElemento.ifPresent(System.out::println);  // Imprimir el primer elemento si está presente

En este ejemplo, findFirst se utiliza para obtener el primer elemento de la lista.

findAny()

La operación findAny es una operación terminal que devuelve cualquier elemento del Stream en un Optional. Si el Stream está vacío, devuelve un Optional vacío.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Obtener cualquier elemento
Optional<String> cualquierElemento = listaDeCadenas.stream().findAny();

cualquierElemento.ifPresent(System.out::println);  // Imprimir cualquier elemento si está presente

En este ejemplo, findAny se utiliza para obtener cualquier elemento de la lista.

forEach()

La operación forEach es una operación terminal que ejecuta una acción para cada elemento del Stream.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Imprimir cada elemento
listaDeCadenas.stream().forEach(System.out::println);

En este ejemplo, forEach se utiliza para imprimir cada elemento de la lista.

collect()

La operación collect es una operación terminal que transforma los elementos del Stream en una estructura de datos diferente, como una List, Set, o Map. Se utiliza junto con la interfaz Collector.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Recoger elementos en una List
List<String> listaRecolectada = listaDeCadenas.stream().collect(Collectors.toList());

// Imprimir elementos
listaRecolectada.forEach(System.out::println);

En este ejemplo, collect se utiliza para recolectar los elementos en una List.

count()

La operación count es una operación terminal que devuelve el número de elementos en el Stream.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Contar elementos
long cantidad = listaDeCadenas.stream().count();

System.out.println(cantidad);  // Salida: 3

En este ejemplo, count se utiliza para contar el número de elementos en la lista.

reduce()

La operación reduce es una operación terminal que combina los elementos del Stream en un solo resultado mediante una función asociativa y un valor identidad. Puede devolver un Optional si el Stream está vacío.

List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);

// Sumar todos los elementos
Optional<Integer> suma = numeros.stream().reduce(Integer::sum);

suma.ifPresent(System.out::println);  // Imprimir la suma si está presente

En este ejemplo, reduce se utiliza para sumar todos los elementos de la lista.

min() y max()

Las operaciones min y max son operaciones terminales que devuelven el elemento mínimo y máximo del Stream, respectivamente, basándose en el orden natural o un comparador proporcionado.

List<Integer> numeros = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);

// Encontrar el mínimo y el máximo
Optional<Integer> minimo = numeros.stream().min(Comparator.naturalOrder());
Optional<Integer> maximo = numeros.stream().max(Comparator.naturalOrder());

minimo.ifPresent(System.out::println);  // Imprimir el mínimo si está presente
maximo.ifPresent(System.out::println);  // Imprimir el máximo si está presente

En este ejemplo, min y max se utilizan para encontrar el elemento mínimo y máximo de la lista.

forEachOrdered()

La operación forEachOrdered es similar a forEach, pero garantiza que los elementos se procesen en orden en un Stream paralelo.

List<String> listaDeCadenas = Arrays.asList("apple", "banana", "orange");

// Imprimir cada elemento en orden en un Stream paralelo
listaDeCadenas.parallelStream().forEachOrdered(System.out::println);

En este ejemplo, forEachOrdered se utiliza para imprimir cada elemento en orden en un Stream paralelo.

Resumen

En Java, los Streams proporcionan una forma declarativa y funcional de procesar colecciones de datos. Las operaciones de stream se dividen en dos categorías: operaciones no terminales y operaciones terminales. Las operaciones no terminales se componen para formar una cadena de operaciones que se ejecutan perezosamente. La ejecución se inicia solo cuando se encuentra una operación terminal.

Las operaciones no terminales, como filter, map, flatMap, distinct, limit, y peek, permiten filtrar, transformar y manipular los elementos del Stream. Las operaciones terminales, como anyMatch, allMatch, noneMatch, findFirst, findAny, forEach, collect, count, reduce, min, max, y forEachOrdered, producen un resultado final o realizan una acción final en los elementos del Stream.

Al aprovechar las operaciones de stream, puedes escribir código más conciso, legible y eficiente cuando trabajas con colecciones de datos en Java.

Última actualización: 23.09.2025

00.02 Descarga de archivos de Internet


1. Introducción

Muchos de los siguientes ejercicios trabajan con archivos JSON de APIs JSON públicas o abiertas de Internet.

Existen varios modos de acceder a recursos, archivos, de Internet:

  • Java IO.
  • Java NIO.
  • Bibliotecas externas como AsyncHttpClient y Apache Commons IO.

2. Java IO

La API más básica que podemos usar para descargar un archivo es Java IO. Podemos utilizar la clase URL para abrir una conexión al archivo que queremos descargar.

Para leer de manera eficaz el archivo, podemos utilizar el método openStream() para obtener un InputStream:

BufferedInputStream in = new BufferedInputStream(
    new URL(FILE_URL).openStream())

o:

URL url = new URL(FILE_URL);
URLConnection conn = url.openConnection();

BufferedReaderBufferedReader br = new BufferedReader(
                   new InputStreamReader(conn.getInputStream()));

La ventaja de este método es que podemos pasarle parámetros (cookies, tokens, identificadores de sesión…) a la cabecera HTTP:

URL url = new URL(FILE_URL);
HttpURLConnection urlc = (HttpURLConnection) url.openConnection(); 
urlc.setInstanceFollowRedirects(true);
urlc.setRequestProperty("User-Agent", "");
urlc.connect();

BufferedReader in = 
new BufferedReader(new InputStreamReader(urlc.getInputStream()));

Al leer desde un InputStream, se recomienda encapsularlo en un BufferedInputStream para aumentar el rendimiento.

Como hemos visto, la mejora de rendimiento proviene del almacenamiento en búfer. Al leer un byte a la vez mediante el método read(), cada llamada al método implica una llamada al sistema al sistema de archivos subyacente. Cuando la JVM invoca la llamada al sistema read(), el contexto de ejecución del programa cambia de modo usuario a modo kernel y viceversa. Este cambio de contexto es costoso desde una perspectiva de rendimiento. Al leer un gran número de bytes, el rendimiento de la aplicación será deficiente debido al gran número de cambios de contexto involucrados.

Para escribir los bytes leídos desde la URL en nuestro archivo local, utilizaremos el método write() de la clase FileOutputStream:

try (BufferedInputStream in = new BufferedInputStream(
    new URL(FILE_URL).openStream());
    FileOutputStream fileOutputStream = 
        new FileOutputStream(FILE_NAME)) {
    byte dataBuffer[] = new byte[1024];
    int bytesRead;
    while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
        fileOutputStream.write(dataBuffer, 0, bytesRead);
    }
} catch (IOException e) {
    // gestión de la excepción
}

Cuando se utiliza un BufferedInputStream, el método read() leerá tantos bytes como hayamos establecido para el tamaño del búfer. En nuestro ejemplo, ya estamos haciendo esto al leer bloques de 1024 bytes a la vez, por lo que BufferedInputStream no es necesario. Para archivos JSON, como son archivos de texto, precisamos convertir el flujo entrada (InputStream) en un Reader por medio de la clase InputStreamReader, pasando a leer línea a línea.

Muchos métodos de procesado de archivos JSON (fromJson, parse,…) tienen una versión sobrecargada que recoge un Reader además de un String.

3. Java NIO

En el caso anterior, bajamos a nivel de flujo pero, como hemos estudiado, a partir de Java 7, se dispone de la clase Files que contiene métodos auxiliares para manejar operaciones de entrada/salida (IO).

Se puede utilizar el método Files.copy() para leer todos los bytes de un InputStream y copiarlos a un archivo local:

InputStream in = new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME), 
              StandardCopyOption.REPLACE_EXISTING);

El ejemplo anterior funciona bien, pero puede mejorarse. El principal ¿inconveniente? es que los bytes se almacenan en búfer en la memoria.

Java NIO tiene métodos para transferir bytes directamente entre dos canales sin almacenamiento en búfer.

El paquete Java NIO ofrece la posibilidad de transferir bytes entre dos canales sin almacenarlos en el espacio de memoria de la aplicación.

Para leer el archivo desde nuestra URL, crearemos un ReadableByteChannel a partir del flujo de URL:

ReadableByteChannel readableByteChannel =
         Channels.newChannel(url.openStream());

Los bytes leídos del ReadableByteChannel se transferirán a un FileChannel correspondiente al archivo que se va a descargar:

FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME);
FileChannel fileChannel = fileOutputStream.getChannel();

Puede usarse el método transferFrom() de la clase ReadableByteChannel para descargar los bytes desde la URL dada a nuestro FileChannel:

fileChannel.transferFrom(readableByteChannel, 
                0, Long.MAX_VALUE);

Los métodos transferTo() y transferFrom() son más eficientes que simplemente leer desde un flujo utilizando un búfer. Dependiendo del sistema operativo subyacente, los datos pueden transferirse directamente desde la caché del sistema de archivos a nuestro archivo sin copiar ningún byte en el espacio de memoria de la aplicación.

En sistemas Linux y UNIX, estos métodos utilizan la técnica de copia cero que reduce el número de cambios de contexto entre el modo kernel y el modo usuario.

(Harry Haller) Última actualización: 23.09.2025

03 Implementación de hashCode()


Sobreescribe hashCode cuando sobreescribes equals

Debes seimpre sobreescribir hashCode en cada clase que sobrescriba equals. Si no lo haces se violará el contrato general de hashCode, lo que hará que NO funcione correctamente en colecciones como HashMap y HashSet. Aquí está el contrato, adaptado de la especificación de Object:

  • Cuando se invoca el método hashCode en un objeto repetidamente durante la ejecución de una aplicación, debe devolver consistentemente el mismo valor, siempre que la información utilizada en las comparaciones de equals no se modifique. Este valor no necesita ser consistente de una ejecución de una aplicación a otra.

  • Si dos objetos son iguales según el método equals(Object), llamar a hashCode en los dos objetos debe producir el mismo resultado entero.

  • Si dos objetos son diferentes según el método equals(Object), no es necesario que llamar a hashCode en cada uno de los objetos produzca resultados distintos. Sin embargo, el programador debe ser consciente de que producir resultados distintos para objetos diferentes puede mejorar el rendimiento de las tablas hash.

La disposición clave que se viola cuando no se sobrescribe hashCode es la segunda: los objetos iguales deben tener códigos hash iguales.
Dos instancias distintas pueden ser lógicamente iguales según el método equals de una clase, pero el método hashCode de Object, son sólo dos objetos con poco en común. Por lo tanto, el método hashCode de Object devuelve dos números aparentemente aleatorios en lugar de dos números iguales, como exige el contrato.

Por ejemplo, añadimos una instancia de la clase NumeroTelefono como clave en un HashMap:

Map<NumeroTelefono, String> m = new HashMap<>();
m.put(new NumeroTelefono(707, 867, 5309), "Otto");

En este punto, podrías esperar que m.get(new NumeroTelefono(707, 867, 5309)) devuelva “Otto”, pero en cambio, devuelve null. Observa que hay dos instancias de NumeroTelefono involucradas: una se utiliza para la inserción en el HashMap, y una segunda instancia igual se utiliza para la recuperación (intentada). La falta de sobrescritura de hashCode en la clase NumeroTelefono hace que las dos instancias iguales tengan códigos hash diferentes, violando el contrato de hashCode. Por lo tanto, es probable que el método get busque el número de teléfono en una tabla hash diferente al que fue almacenado por el método put. Incluso si las dos instancias resultan en el mismo cubo hash, es muy probable que el método get devuelva null, porque HashMap tiene una optimización que almacena en caché el código hash asociado con cada entrada y no se molesta en verificar la igualdad de objetos si los códigos hash no coinciden.

Para solucionar este problema, es tan sencillo como escribir un método hashCode adecuado para NumeroTelefono. Entonces, ¿cómo debería implementarse un método hashCode? Es fácil escribir uno malo. Este, por ejemplo, siempre es legal pero nunca debería ser usado:

// La peor implementación legal posible de hashCode: ¡nunca uses esto!
@Override
public int hashCode() {
    return 42;
}

Es legal porque asegura que los objetos iguales tengan el mismo código hash. Es muy nmalo porque asegura que cada objeto tenga el mismo código hash. Por lo tanto, cada objeto se asigna a la misma tabla hash, y las tablas hash degeneran en listas enlazadas. Los programas que deberían ejecutarse en tiempo lineal en su lugar se ejecutan en tiempo cuadrático. Para tablas hash grandes, esta es la diferencia entre funcionar y no funcionar.

Una buena función de hash tiende a producir códigos hash diferentes para instancias diferentes. Esto es exactamente lo que se entiende por la tercera parte del contrato de hashCode. Idealmente, una función de hash debería distribuir uniformemente cualquier colección razonable de instancias diferentes en todos los valores int. Lograr este ideal puede ser difícil, pero afortunadamente, no es demasiado difícil lograr una aproximación justa. Aquí tienes una receta sencilla:

  1. Declara una variable int llamada resultado y inicialízala con el código hash c para el primer campo significativo en tu objeto, calculado en el paso 2.1. (Un campo significativo es un campo que afecta las comparaciones de equals).

  2. Para cada campo significativo restante f en tu objeto, realiza lo siguiente:

    1. 2.1 Calcula un código hash c para el campo:
      1. i. Si el campo es de un tipo primitivo, calcula Type.hashCode(f), donde Type es la clase primitiva encapsulada correspondiente al tipo de f.
      2. Si el campo es una referencia a un objeto y el método equals de esta clase compara el campo mediante la invocación recursiva de equals, invoca recursivamente hashCode en el campo. Si se requiere una comparación más compleja, calcula una “representación canónica” para este campo e invoca hashCode en la representación canónica. Si el valor del campo es null, utiliza 0 (u otra constante, pero 0 es tradicional).
      3. Si el campo es un array, trátalo como si cada elemento significativo fuera un campo separado. Es decir, calcula un código hash para cada elemento significativo aplicando estas reglas recursivamente y combina los valores según el paso 2.b. Si el array no tiene elementos significativos, utiliza una constante, preferiblemente no 0. Si todos los elementos son significativos, utiliza Arrays.hashCode.
    2. Combina el código hash c calculado en el paso 2.a en resultado de la siguiente manera:
    resultado = 31 * resultado + c;
  3. Devuelve resultado.

Cuando hayas terminado de escribir el método hashCode, pregúntate a ti mismo si las instancias iguales tienen códigos hash iguales. Escribe pruebas unitarias para verificar tu intuición (a menos que hayas utilizado AutoValue para generar tus métodos equals y hashCode, en cuyo caso puedes omitir estas pruebas de manera segura). Si las instancias iguales tienen códigos hash diferentes, averigua por qué y soluciona el problema.

Puedes excluir campos derivados del cálculo del código hash. En otras palabras, puedes ignorar cualquier campo cuyo valor pueda calcularse a partir de campos incluidos en el cálculo. Debes excluir cualquier campo que no se utilice en comparaciones de equals, o corres el riesgo de violar la segunda disposición del contrato de hashCode.

La multiplicación en el paso 2.b hace que el resultado dependa del orden de los campos, lo que produce una función de hash mucho mejor si la clase tiene múltiples campos similares. Por ejemplo, si se omitiera la multiplicación de una función de hash de String, todos los anagramas tendrían códigos hash idénticos. Se eligió el valor 31 porque es un número primo impar. Si fuera par y la multiplicación desbordara, se perdería información, ya que la multiplicación por 2 es equivalente a un desplazamiento. La ventaja de usar un número primo es menos clara, pero es tradicional. Una propiedad agradable del 31 es que la multiplicación se puede reemplazar por un desplazamiento y una resta para obtener un mejor rendimiento en algunas arquitecturas: 31 * i == (i << 5) - i. Las VM modernas realizan este tipo de optimización automáticamente.

Aplicaremos la receta anterior a la clase NumeroTelefono:

// Método típico de hashCode
@Override
public int hashCode() {
    int resultado = Short.hashCode(codigoArea);
    resultado = 31 * resultado + Short.hashCode(prefijo);
    resultado = 31 * resultado + Short.hashCode(numeroT);
    return resultado;
}

Dado que este método devuelve el resultado de un cálculo determinista simple cuyas únicas entradas son los tres campos significativos en una instancia de NumeroTelefono, es evidente que las instancias iguales de NumeroTelefono tienen códigos hash iguales. Este método es, de hecho, una implementación de hashCode perfectamente buena para NumeroTelefono, al nivel de las bibliotecas de plataformas Java. Es simple, bastante rápido y hace un trabajo razonable al dispersar números de teléfono diferentes en diferentes cubos hash.

Si bien esta estrategia produce funciones de hash bastante buenas, no son de última generación. Son comparables en calidad a las funciones de hash que se encuentran en los tipos de valor de las bibliotecas de plataformas Java y son adecuadas para la mayoría de los usos. Si realmente necesitas funciones de hash menos propensas a producir colisiones, consulta la clase com.google.common.hash.Hashing de Guava [Guava].

La clase Objects tiene un método estático que recoge un número arbitrario de objetos y devuelve un código hash para ellos. Este método, llamado hash, te permite escribir métodos hashCode de una línea cuya calidad es comparable a los declarados anteriormente. Desafortunadamente, se ejecutan más lentamente porque implican la creación de un array para pasar un número variable de argumentos, así como el boxing y unboxing si alguno de los argumentos es de tipo primitivo. Este estilo de función de hash se recomienda sólo en situaciones donde el rendimiento no es crítico. Aquí tienes una función de hash para NumeroTelefono escrita utilizando esta técnica:

// Método hashCode de una línea - rendimiento mediocre
@Override
public int hashCode() {
    return Objects.hash(numeroT, prefijo, codigoArea);
}

Si una clase es inmutable y el costo de calcular el código hash es significativo, podrías considerar almacenar en caché el código hash en el objeto en lugar de recalcularlo cada vez que se solicita. Si crees que la mayoría de los objetos de este tipo se utilizarán como claves hash, deberías calcular el código hash cuando se crea la instancia. De lo contrario, podrías optar por inicializar perezosamente el código hash la primera vez que se invoca hashCode. Se requiere cierto cuidado para asegurar que la clase siga siendo segura para subprocesos en presencia de un campo inicializado de forma perezosa. Nuestra clase NumeroTelefono no merece este tratamiento, pero solo para mostrarte cómo se hace, aquí está. Ten en cuenta que el valor inicial para el campo hashCode (en este caso, 0) no debería ser el código hash de una instancia comúnmente creada:

// Método hashCode con caché de código hash inicializado perezosamente
private int hashCode; // Inicializado automáticamente a 0
@Override
public int hashCode() {
    int resultado = hashCode;
    if (resultado == 0) {
        resultado = Short.hashCode(codigoArea);
        resultado = 31 * resultado + Short.hashCode(prefijo);
        resultado = 31 * resultado + Short.hashCode(numeroT);
        hashCode = resultado;
    }
    return resultado;
}

No te dejes tentar a excluir campos significativos del cálculo del código hash para mejorar el rendimiento. Aunque la función de hash resultante puede ejecutarse más rápido, su baja calidad puede degradar el rendimiento de las tablas hash hasta el punto en que se vuelven inutilizables. En particular, la función de hash puede enfrentarse a una gran colección de instancias que difieren principalmente en las regiones que has elegido ignorar. Si esto sucede, la función de hash asignará todas estas instancias a unos pocos códigos hash, y los programas que deberían ejecutarse en tiempo lineal en su lugar se ejecutarán en tiempo cuadrático.

Esto no es solo un problema teórico. Antes de Java 2, la función de hash de String utilizaba como máximo dieciséis caracteres distribuidos uniformemente en toda la cadena, comenzando por el primer carácter. Para grandes colecciones de nombres jerárquicos, como las URL, esta función mostraba exactamente el comportamiento patológico descrito anteriormente.

No proporciones una especificación detallada para el valor devuelto por hashCode, de modo que los clientes no puedan depender razonablemente de él; esto te da la flexibilidad para cambiarlo. Muchas clases en las bibliotecas de Java, como String e Integer, especifican el valor exacto devuelto por su método hashCode como una función del valor de la instancia. Esto no es una buena idea, sino un error con el que nos vemos obligados a vivir: obstaculiza la capacidad de mejorar la función de hash en futuras versiones. Si dejas los detalles sin especificar y se encuentra un defecto en la función de hash o se descubre una función de hash mejor, puedes cambiarla en una versión posterior.

En resumen, debes sobrescribir hashCode cada vez que sobres equals, o tu programa no funcionará correctamente. Tu método hashCode debe obedecer el contrato general especificado en Object y debe hacer un trabajo razonable asignando códigos hash diferentes a instancias diferentes. Esto es fácil de lograr, aunque ligeramente tedioso, utilizando la receta en la página 51. Como se menciona en el Ítem 10, el framework AutoValue proporciona una excelente alternativa para escribir manualmente los métodos equals y hashCode, y los IDE también ofrecen parte de esta funcionalidad.

(Harry Haller) Última actualización: 23.09.2025

03. Layouts en Java Swing & UI

08.04. Layouts en swing

Clases de Layout en Swing

Existen varias clases Swing (algunas heredadas de java.awt) que proporcionan gestores de diseño (Layouts) para uso general:

Nota: nosotros aprenderemos a situar los elementos con código de diseño a mano, lo cual puede ser desafiante, pero muy útil para aprender conceptos y poder después tocar el código que proporcionan los IDE. Los IDE, como el Netbeans, suelen emplear un GroupLayout. Para codificar a mano el GridBagLayout se recomienda como el Layout más flexible y poderoso.


BorderLayout

Un BorderLayout coloca componentes en hasta cinco áreas: arriba, abajo, izquierda, derecha y centro. Todo el espacio adicional se coloca en el área central. Las barras de herramientas creadas con JToolBar deben ser creadas dentro de un contenedor BorderLayout si deseas poder arrastrar y soltar las barras desde sus posiciones iniciales.

BoxLayout

La clase BoxLayout coloca componentes en una sola fila o columna. Respeta los tamaños máximos solicitados por los componentes y también te permite alinear componentes.

CardLayout

La clase CardLayout permite implementar un área que contiene diferentes componentes en diferentes momentos. Un CardLayout suele ser controlado por un cuadro combinado, con el estado del cuadro combinado determinando qué panel (grupo de componentes) muestra el CardLayout. Una alternativa a usar CardLayout es usar un panel con pestañas, que proporciona una funcionalidad similar pero con una GUI predefinida. Por ello, lo más sencillo es utilizar un JTabbedPane y no este Layout.

FlowLayout

FlowLayout es el gestor de diseño predeterminado para cada JPanel. Simplemente distribuye los componentes en una sola fila, comenzando una nueva fila si su contenedor no es lo suficientemente ancho.

GridBagLayout

GridBagLayout es un gestor de diseño sofisticado y flexible. Alinea los componentes colocándolos dentro de una cuadrícula de celdas, permitiendo que los componentes abarquen más de una celda. Las filas en la cuadrícula pueden tener diferentes alturas, y las columnas de la cuadrícula pueden tener diferentes anchos.

GridLayout

GridLayout simplemente hace que un grupo de componentes en forma de rejilla tengan el mismo tamaño y los muestra en el número solicitado de filas y columnas.

GroupLayout (*)

GroupLayout es un gestor de diseño que fue desarrollado para ser usado por herramientas de construcción de GUI, pero también puede ser usado manualmente. GroupLayout trabaja con los diseños horizontal y vertical por separado. El diseño se define para cada dimensión independientemente. En consecuencia, cada componente necesita ser definido dos veces en el diseño.

SpringLayout (*)

SpringLayout es un gestor de diseño flexible diseñado para ser usado por constructores de GUI. Te permite especificar relaciones precisas entre los bordes de los componentes bajo su control. Por ejemplo, podrías definir que el borde izquierdo de un componente está a cierta distancia (que puede ser calculada dinámicamente) del borde derecho de un segundo componente. SpringLayout distribuye los hijos de su contenedor asociado según un conjunto de restricciones.

1. BorderLayout

Cada contenedor de nivel superior (JFrame, JDialog,… ) de contenido se inicializa por defecto a BorderLayout.

Un BorderLayout coloca componentes en hasta cinco áreas: superior, inferior, izquierda, derecha y centro. Todo el espacio adicional se coloca en el área central. Las barras de herramientas creadas usando JToolBar deben crearse dentro de un contenedor BorderLayout si se desea poder arrastrar y soltar las barras desde sus posiciones iniciales.

2. BoxLayout

La clase BoxLayout coloca los componentes en una sola fila o columna. Respeta los tamaños máximos solicitados por los componentes y también permite alinear los componentes.

3. CardLayout

La clase CardLayout te permite implementar un área que contiene diferentes componentes en diferentes momentos. Un CardLayout suele ser controlado por un cuadro combinado (combo box), con el estado del cuadro combinado determinando qué panel (grupo de componentes) muestra el CardLayout. Una alternativa a usar CardLayout es usar un panel con pestañas, un JTabbedPane, que proporciona una funcionalidad similar pero con una GUI predefinida.

4. FlowLayout

FlowLayout es el gestor de diseño predeterminado para cada JPanel. Simplemente organiza los componentes en una sola fila, comenzando una nueva fila si su contenedor no es lo suficientemente ancho. Ambos paneles en CardLayoutDemo, mostrados anteriormente, usan FlowLayout.

5. GridBagLayout

GridBagLayout es un gestor de diseño sofisticado y flexible. Alinea los componentes colocándolos dentro de una cuadrícula de celdas, permitiendo que los componentes abarquen más de una celda. Las filas en la cuadrícula pueden tener diferentes alturas y las columnas de la cuadrícula pueden tener diferentes anchos.

6. GridLayout

GridLayout simplemente hace que un montón de componentes sean del mismo tamaño y los muestra en el número solicitado de filas y columnas.

7. GroupLayout

GroupLayout es un gestor de diseño que fue desarrollado para su uso por herramientas de construcción de GUI, pero también puede ser utilizado manualmente. GroupLayout trabaja con los diseños horizontales y verticales por separado. El diseño se define para cada dimensión de manera independiente. Por lo tanto, cada componente necesita ser definido dos veces en el diseño. La ventana de búsqueda mostrada anteriormente es un ejemplo de un GroupLayout.

8. SpringLayout

SpringLayout es un gestor de diseño flexible diseñado para ser utilizado por constructores de GUI. Permite especificar relaciones precisas entre los bordes de los componentes bajo su control. Por ejemplo, se puede definir que el borde izquierdo de un componente esté a cierta distancia (que puede calcularse dinámicamente) del borde derecho de un segundo componente. SpringLayout organiza los hijos de su contenedor asociado de acuerdo a un conjunto de restricciones.

Última actualización: 23.09.2025

Subsecciones de 03. Layouts en Java Swing & UI

01 Ventanas de entrada de datos, mensajes y archivos

Ventanas de diálogo con Swing

JFC (Java Foundation Classes) es un conjunto de funciones/clases para crear interfaces gráficas de usuario (GUI) y añadir funcionalidad gráfica e interactividad a las aplicaciones Java que estudiaréis en la materia de Desenvolvemento de Interfaces.

Incluye: componentes gráficos de Swing (botones, paneles, tablas, ventanas, etc.), configuración de apariencia, API Java 2D, API de accesibilidad (lectores de pantalla, Braille,…); internacionalización (gestión de idiomas del mundo, …)


En la materia de Acceso a Datos vamos a dar una pequeña introducción a dos componentes de diálogo que nos facilitarán la introducción de datos en la primera parte, mostrar mensajes o selección de ficheros hasta que lo estudiéis en otras materias:


Una ventana de diálogo es una subventana independiente destinada a llevar un aviso temporal además de la ventana principal de la aplicación Swing.

Suelen usarse para mostrar mensajes de error o una advertencia, presentar imágenes, árboles de directorios, etc.

Para facilitar el trabajo, existen las clases de utilidad:

1. Paquetes principales del API

  • javax.swing
  • javax.swing.event

Además de:

javax.accessibility javax.swing.plaf javax.swing.text
javax.swing javax.swing.plaf.basic javax.swing.text.html
javax.swing.colorchooser javax.swing.plaf.multi javax.swing.text.rtf
javax.swing.border javax.swing.plaf.metal javax.swing.text.html.parser
javax.swing.event javax.swing.plaf.synth javax.swing.tree
javax.swing.filechooser javax.swing.table javax.swing.undo

2. Componentes de Swing

Componentes de Swing Componentes de Swing

3. JOptionPane

JOptionPane permite crear y personalizar rápidamente varios tipos diferentes de cuadros de diálogo.

Proporciona soporte para diseñar cuadros de diálogo estándar, con iconos, especificar el título y el texto del cuadro de diálogo y personalizar el texto del botón.

La compatibilidad con iconos de le permite especificar fácilmente qué icono muestra el cuadro de diálogo.

Son modales, esto es, bloquea el acceso a la ventana padre.

Puede utilizar un icono personalizado, ningún icono o cualquiera de los cuatro iconos estándar:

Iconos de JOptionPane Iconos de JOptionPane

Cada apariencia tiene sus propias versiones de los cuatro íconos estándar.

La forma más sencilla de crear y mostrar diálogos con JOptionPane es por medio de los métodos showXxxDialog:

Método Descripción
showConfirmDialog Pregunta de confirmación, como sí/no/cancelar.
showInputDialog Solicita entrada de datos.
showMessageDialog Muestra un mensaje informativo
showOptionDialog Permite personalizar el JOptionPane.

Existe una versión para marcos internos, JInternalFrame con métodos de la forma: showInternalXxxx

El formato del mensaje tiene una apariencia siguiendo la siguiente estructura:**

Dialogo común Dialogo común

Mensaje saludo Mensaje saludo

Los parámetros de los método showXxxDialog son los siguientes:

  • parentComponent: componente padre, el JFrame. Si el valor es null, se usa el JFrame por defecto y se centra en la pantalla.

  • mensaje: mensaje de la ventana de diálogo. Normalmente String, pero puede ser cualquier objeto:

    • Object[]: será interpretado como una serie de mensajes (uno por objeto) situados en vertical.
    • Component: mostrará el componente en la ventana de diálogo.
    • Icon: el icono será mostrado dentro de un JLabel.
    • Otros: se convierten a String llamando al método toString() y mostrándolo dentro de un JLabel.
  • Título: título de la ventana.

  • messageType: define el estilo del mensaje. Los posibles valores son:

    • ERROR_MESSAGE
    • INFORMATION_MESSAGE
    • WARNING_MESSAGE
    • QUESTION_MESSAGE
    • PLAIN_MESSAGE
  • optionType: conjunto botones de opción que aparen debajo de la ventana. Se pueden proporcionar otro botones usando el parámetro options.

    • DEFAULT_OPTION
    • YES_NO_OPTION
    • YES_NO_CANCEL_OPTION
    • OK_CANCEL_OPTION
  • options: es una descripción más detallada del conjunto de botones que aparecen en la parte inferior de la ventana. Lo usual es un array de String, pero puede ser un array de Object:

    • Component: el componente se añade a la lista de botones.
      • Icon: se crea un JButton con este icono.
    • Otros: el objeto se convierte en String (toString()) y se emplea como etiqueta del botón.
  • Icono: icono de la ventana de diálogo. El valor por defecto está determinado por el tipo de mensaje.

  • initialValue: valor por defecto para ventanas de tipo input.

Los métodos showXxxDialog devuelven un entero, cuyos valores posibles son las constantes que referencian al botón pulsado:

  • YES_OPTION
  • NO_OPTION
  • CANCEL_OPTION
  • OK_OPTION
  • CLOSED_OPTION
Object[] opcionesBoton = {"Sí, por supuesto", "No, gracias", "No estoy loco!"};
int resultado = JOptionPane.showOptionDialog(this, "¿Te has vacunado de COVID?", // mensaje
    "Una pregunta impertinente", // título
        JOptionPane.YES_NO_CANCEL_OPTION, // OptionType
        JOptionPane.QUESTION_MESSAGE, // Tipo de mensaje null, // icono
        opcionesBoton,
            opcionesBoton[2]); // valor por defecto

Mensaje título e icono Mensaje título e icono

showMessageDialog

showMessageDialog : muestra un mensaje simple con un botón. (this es la referencia al formulario)

JOptionPane.showMessageDialog(this, "Hola Pepe",
                                      "Un saludo a Harry Haller",
                              JOptionPane.INFORMATION_MESSAGE);

Mensaje Mensaje

Con título e icono por defecto:

JOptionPane.showMessageDialog(this, "Ovos fritidos con paracas.");

Con título e icono por defecto Con título e icono por defecto

Con título, icono de aviso:

JOptionPane.showMessageDialog(this, "Arroz con chícharos.", "Aviso", JOptionPane.WARNING_MESSAGE);

Con título, icono de aviso Con título, icono de aviso

Con título, icono de error:

JOptionPane.showMessageDialog(this, "Repolo de Vimianzo.", "Erro", JOptionPane.ERROR_MESSAGE);

Con título, icono de error Con título, icono de error

Con título, sin icono:

JOptionPane.showMessageDialog(this, "Caldo galego.", "Nada", JOptionPane.PLAIN_MESSAGE);

Con título, sin icono Con título, sin icono

Con título, icono personalizado:

ImageIcon icoSalada = new ImageIcon( getClass().getResource("/images/ensalada.png"));
JOptionPane.showMessageDialog(this, "Salada con verde.", "Primeiro prato", JOptionPane.INFORMATION_MESSAGE,  iconaSalada);

Con título, sin icono Con título, sin icono

showInputDialog

showInputDialog : es el único método showXxxDialog que no devuelve un entero.

Devuelve un objeto, normalmente un String:

Object[] platoFavorito = {"chícharos", "doce", "churros"};
String s = (String)JOptionPane.showInputDialog(this, "o meu prato favorito é: ", 
        "Introduce tu plato favorito", JOptionPane.PLAIN_MESSAGE, iconaSalada, platoFavorito, "churros");

Devulver String Devulver String

Si ponemos null en el array de opciones aparecerá una caja de texto:

Devuelve un objeto, normalmente un String:

String s = (String)JOptionPane.showInputDialog(this,
    "o meu prato favorito é: ", "Introduce tu plato favorito", JOptionPane.PLAIN_MESSAGE, iconaSalada,
    null,  "churros");

Input String Input String

4. JFileChooser

Los selectores de archivos proporcionan una GUI para navegar por el sistema de archivos y luego elegir un archivo o directorio de una lista, o introducir el nombre de un archivo o directorio.  Normalmente usa la clase JFileChooser para mostrar un cuadro de diálogo modal que contiene el selector de archivos. Otra forma de presentar un selector de archivos es agregar una instancia de JFileChooser a un contenedor (ventana etc)

Ejemplo:

JFileChooser fc = new JFileChooser();
int returnVal = fc.showOpenDialog(this);

if (returnVal == JFileChooser.APPROVE_OPTION) {
    File file = fc.getSelectedFile();
    // Esto es lo que se hace con el archivo seleccionado
} else {
    // Esto es lo que se hace si se cancela la selección
}

El método showOpenDialog muestra un cuadro de diálogo para abrir un archivo.

JFileChooser showOpenDialog JFileChooser showOpenDialog

El método showSaveDialog muestra un cuadro de diálogo para guardar un archivo.

JFileChooser showSaveDialog JFileChooser showSaveDialog

JFileChooser fcImaxe = new JFileChooser();
FileNameExtensionFilter filtro 
        = new FileNameExtensionFilter("Imágenes JPG y PNG", "jpg", "png");
fcImaxe.setFileFilter(filtro);
int valorSel = fcImaxe.showOpenDialog(null);
if (valorSel == JFileChooser.APPROVE_OPTION){
    System.out.println("Has seleccionado la imagen:" + fcImaxe.getSelectedFile().getName());
}

Directorio de trabajo

showOpenDialog recoge el componente padre de la ventana de diálogo y afecta a la posición de la ventana de diálogo.

Por defecto muestra los archivos del directorio de trabajo del usuario, pero puede especificarse el directorio inicial de varios modos:

  • En el constructor:
JFileChooser fc = new JFileChooser("e:\\");
  • Por medio del método:
fc.setCurrentDirectory(new File("e:\\"));

Selección de archivos y/o directorios

showOpenDialog/showSaveDialog recoge devuelven un entero que indica si se ha seleccionado un archivo: APPROVE_OPTION o CANCEL_OPTION

Una vez seleccionado un archivo o directorio (en ese caso debe indicarse que se permite selección de directorios) puede invocarse al método getSelectedFile() para recuperar el archivo (File):

File arquivo = fcImaxe.getSelectedFile();

Una vez recuperado el archivo podemos obtener muchos datos del mismo (lo veremos en la unidad de archivos):

File arquivo = fcImaxe.getSelectedFile();
arquivo.getPath();
arquivo.getName();
arquivo.isDirectory();
arquivo.exists();
arquivo.delete();
// ...

Se puede utilizar la misma instancia de la JFileChooser para mostrar un cuadro de diálogo estándar para guardar.

int valor = fc.showSaveDialog(null);

Al utilizar la misma instancia del JFileChooser:

  • Recuerda el directorio actual entre usos, por lo que las versiones para abrir y guardar comparten automáticamente el mismo directorio actual.
  • Sólo se personaliza un selector de archivos, y las personalizaciones se aplican tanto a la versión para abrir como para guardar.

Se puede cambiar el modo de selección de archivos, por ejemplo para seleccionar directorios:

fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

Además, existen otros modos de selección:

  • FILES_AND_DIRECTORIES.
  • FILES_ONLY (por defecto).
  • DIRECTORIES_ONLY.

Filtros de archivos

Por defecto, el JFileChooser muestra todos los archivos y directorios, excepto los ocultos.

Pueden programarse filtros de archivos para escoger algún tipo de archivo o directorio.

El JFileChooser llama al método accept (de FileFilter) determina qué se mostrará.

Existen varios tipos de filtros:

FileFilter filtro = new FileNameExtensionFilter("archivo JPEG", "jpg", "jpeg");
 JFileChooser fc; // = ...;
 fc.setFileFilter(filtro);
// fc.addChoosableFileFilter(filtro); // agrega a los seleccionables.
// fc. setAcceptAllFileFilterUsed(false);
Última actualización: 23.09.2025

05 GridBagLayout

GridBagLayout

Cómo usar GridBagLayout

GridBagLayout es uno de los gestores de diseño más flexibles —y complejos— que proporciona la plataforma Java. Un GridBagLayout coloca componentes en una cuadrícula de filas y columnas, permitiendo que los componentes especificados abarquen múltiples filas o columnas. No todas las filas necesariamente tienen la misma altura. Del mismo modo, no todas las columnas necesariamente tienen el mismo ancho. Esencialmente, GridBagLayout coloca los componentes en rectángulos (celdas) en una cuadrícula y luego utiliza los tamaños preferidos de los componentes para determinar qué tan grandes deben ser las celdas.

La forma en que el programa especifica las características de tamaño y posición de sus componentes es especificando restricciones para cada componente. El enfoque preferido para establecer restricciones en un componente es usar la variante de Container.add, pasándole un objeto GridBagConstraints.

GridBagLayout y GridBagConstraints

GridBagConstraints es una clase que especifica cómo se colocan los componentes en un GridBagLayout. Cada componente que se agrega a un contenedor con un GridBagLayout debe tener su propio objeto GridBagConstraints.

GridBagConstraints tiene muchas propiedades que se pueden establecer para controlar cómo se coloca un componente en un GridBagLayout. Algunas de las propiedades más comunes son:

  • gridx, gridy: la posición de la celda en la cuadrícula donde se colocará el componente.
  • gridwidth, gridheight: el número de celdas en la cuadrícula que el componente debe ocupar en la dirección horizontal y vertical.
  • weightx, weighty: la cantidad de espacio adicional que se asigna a la celda en la dirección horizontal y vertical.
  • fill: cómo el componente debe llenar la celda si la celda es más grande que el componente.
  • anchor: cómo el componente debe ser posicionado en la celda si la celda es más grande que el componente.
  • insets: el espacio adicional que se debe agregar alrededor del componente.
  • ipadx, ipady: el espacio adicional que se debe agregar alrededor del componente en la dirección horizontal y vertical.

Por ejemplo, el siguiente código Swing crea un GridBagLayout con un botón en la esquina superior izquierda de la cuadrícula:

import java.awt.*;
import javax.swing.*;

public class GridBagLayoutExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Ejemplo de GridBagLayout");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panel = new JPanel(new GridBagLayout());
        frame.add(panel);

        JButton button = new JButton("Button");
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;
        panel.add(button, constraints);

        frame.setSize(300, 200);
        frame.setVisible(true);
    }
}

En este ejemplo, se crea un GridBagLayout y se agrega un botón al panel. El botón se coloca en la esquina superior izquierda de la cuadrícula, ya que gridx y gridy se establecen en 0.

También podríamos crear un rejilla de 3x3 y añadir un botón en la posición (1,1) de la cuadrícula:

import java.awt.*;

import javax.swing.*;

public class GridBagLayoutExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Ejemplo de GridBagLayout");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panel = new JPanel(new GridBagLayout());
        frame.add(panel);

        JButton button = new JButton("Button");
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.gridx = 1;
        constraints.gridy = 1;
        panel.add(button, constraints);

        frame.setSize(300, 200);
        frame.setVisible(true);
    }
}

En este caso, el botón se coloca en la posición (1,1) de la cuadrícula, ya que gridx y gridy se establecen en 1.

Otro ejemplo más complejo podría ser el siguiente:

import java.awt.*;
import javax.swing.*;

public class GridBagLayoutExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Ejemplo de GridBagLayout");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panel = new JPanel(new GridBagLayout());
        frame.add(panel);

        JButton button1 = new JButton("Button 1");
        GridBagConstraints constraints1 = new GridBagConstraints();
        constraints1.gridx = 0;
        constraints1.gridy = 0;
        panel.add(button1, constraints1);

        JButton button2 = new JButton("Button 2");
        GridBagConstraints constraints2 = new GridBagConstraints();
        constraints2.gridx = 1;
        constraints2.gridy = 0;
        panel.add(button2, constraints2);

        JButton button3 = new JButton("Button 3");
        GridBagConstraints constraints3 = new GridBagConstraints();
        constraints3.gridx = 0;
        constraints3.gridy = 1;
        constraints3.gridwidth = 2;
        panel.add(button3, constraints3);

        frame.setSize(300, 200);
        frame.setVisible(true);
    }
}

En este ejemplo, se crean tres botones y se agregan al panel. El primer botón se coloca en la posición (0,0) de la cuadrícula, el segundo botón se coloca en la posición (1,0) de la cuadrícula y el tercer botón se coloca en la posición (0,1) de la cuadrícula y ocupa dos celdas en la dirección horizontal.

Por último, podemos crear un rejilla de dos filas y hacer que la segunda columna ocupe dos celdas en la dirección vertical:

import java.awt.*;
import javax.swing.*;

public class GridBagLayoutExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Ejemplo de GridBagLayout");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JPanel panel = new JPanel(new GridBagLayout());
        frame.add(panel);

        JButton button1 = new JButton("Button 1");
        GridBagConstraints constraints1 = new GridBagConstraints();
        constraints1.gridx = 0;
        constraints1.gridy = 0;
        panel.add(button1, constraints1);

        JButton button2 = new JButton("Button 2");
        GridBagConstraints constraints2 = new GridBagConstraints();
        constraints2.gridx = 1;
        constraints2.gridy = 0;
        panel.add(button2, constraints2);

        JButton button3 = new JButton("Button 3");
        GridBagConstraints constraints3 = new GridBagConstraints();
        constraints3.gridx = 0;
        constraints3.gridy = 1;
        constraints3.gridheight = 2;
        panel.add(button3, constraints3);

        frame.setSize(300, 200);
        frame.setVisible(true);
    }
}

En este ejemplo, se crean tres botones y se agregan al panel. El primer botón se coloca en la posición (0,0) de la cuadrícula, el segundo botón se coloca en la posición (1,0) de la cuadrícula y el tercer botón se coloca en la posición (0,1) de la cuadrícula y ocupa dos celdas en la dirección vertical.

anchor y fill

Además de las propiedades gridx, gridy, gridwidth, gridheight, weightx, weighty, insets, ipadx e ipady, GridBagConstraints también tiene las propiedades anchor y fill.

La propiedad anchor controla cómo se posiciona un componente en su celda si la celda es más grande que el componente. Los valores posibles para anchor son:

  • GridBagConstraints.FIRST_LINE_START (GridBagConstraints.NORTHWEST): el componente se coloca en la esquina superior izquierda de la celda.

  • GridBagConstraints.PAGE_START (GridBagConstraints.NORTH): el componente se coloca en la parte superior de la celda.

  • GridBagConstraints.FIRST_LINE_END (GridBagConstraints.NORTHEAST): el componente se coloca en la esquina superior derecha de la celda.

  • GridBagConstraints.LINE_START (GridBagConstraints.WEST): el componente se coloca en el lado de inicio de la celda (izquierda en un contenedor de izquierda a derecha, derecha en un contenedor de derecha a izquierda).

  • GridBagConstraints.CENTER: el componente se coloca en el centro de la celda.

  • GridBagConstraints.LINE_END (GridBagConstraints.EAST): el componente se coloca en el lado final de la celda (derecha en un contenedor de izquierda a derecha, izquierda en un contenedor de derecha a izquierda).

  • GridBagConstraints.LAST_LINE_START (GridBagConstraints.SOUTHWEST): el componente se coloca en la esquina inferior izquierda de la celda.

  • GridBagConstraints.PAGE_END (GridBagConstraints.SOUTH): el componente se coloca en la parte inferior de la celda.

  • GridBagConstraints.LAST_LINE_END (GridBagConstraints.SOUTHEAST): el componente se coloca en la esquina inferior derecha de la celda.

  • GridBagConstraints.BASELINE: el componente se coloca en la línea base de la celda.

  • GridBagConstraints.BASELINE_LEADING: el componente se coloca en la línea base de inicio de la celda.

  • GridBagConstraints.BASELINE_TRAILING: el componente se coloca en la línea base de final de la celda.

Conclusión

GridBagLayout es un gestor de diseño muy flexible que permite colocar componentes en una cuadrícula de filas y columnas. Al utilizar GridBagConstraints, se pueden controlar las propiedades de tamaño y posición de los componentes en la cuadrícula. Aunque GridBagLayout puede ser complejo de usar, ofrece una gran flexibilidad para diseñar interfaces de usuario complejas y personalizadas.

Última actualización: 23.09.2025

04. Patrones de diseño

En este apartado, repasaremos los principios de diseño de clases Java y los principales patrones de diseño empleados en el acceso a datos, sobre todo los patrones de creación.

Patrones de diseño

Un patrón de diseño es una solución general establecida para un problema común en el desarrollo de software. El propósito de un patrón de diseño es buscar estrategias comunes para aprovechar el conocimiento y la experiencia acumulada de los desarrolladores para resolver problemas fácilmente. También proporciona a los desarrolladores un vocabulario común en el que pueden discutir problemas y soluciones comunes. Por ejemplo, si dices que escribiste getters/setters o implementaste el patrón singleton, la mayoría de los desarrolladores entenderán la estructura de tu código sin tener que profundizar en los detalles de bajo nivel.

Los patrones que pueden resultar más interesantes para acceso a datos, son los patrones de creación, un tipo de patrón que gestiona la creación de objetos dentro de una aplicación.

Escritor escritor = new Poeta();

EL problema con la creación de objetos radica en cómo crear y gestionar objetos en sistemas más complejos. Por ejemplo, necesitamos saber exactamente qué tipo de objeto es Escritor, en este caso, Poeta, que se crea en tiempo de compilación. Pero, en muchos casos no se sabe hasta tiempo de ejecución, además de crear un solo objeto Escritor en la memoria compartido por todas las clases dentro de nuestra aplicación (patrón Singleton).

Los patrones de creación simplemente aplican un nivel de indirección a la creación de objetos al crear el objeto en alguna otra clase, en lugar de crear el objeto directamente en tu aplicación. El nivel de indirección es un término general para resolver un problema de diseño de software dividiendo conceptualmente la tarea en múltiples niveles.

Última actualización: 23.09.2025

Subsecciones de 04. Patrones de diseño

01. Principios de diseño


Introducción

Haremos un inciso para repasar y descubrir nuevos principios de diseño, pues son un elemento clave en el diseño de estructura de clases de acceso a Datos, patrones como Singleton son interesantes para establecer conexiones a BD, por ejemplo.

Un principio de diseño es una idea establecida o mejor práctica que facilita el proceso de diseño de software. Al crear clases en Java estos principios conducen a bases de código mejores y más manejables. En general, seguir buenos principios de diseño lleva a:

  • Código más lógico
  • Código más fácil de entender
  • Clases más fáciles de reutilizar en otras relaciones y aplicaciones
  • Código más fácil de mantener y que se adapta más fácilmente a cambios en los requisitos de la aplicación

Un modelo de datos es la representación de nuestros objetos y sus propiedades dentro de la aplicación y cómo se relacionan con elementos del mundo real.

Encapsulación de Datos

Un principio fundamental del diseño orientado a objetos es el concepto de encapsular datos.
En el desarrollo de software, la encapsulación es declarar que los atributos y métodos en una clase de manera que los métodos operen sobre los atributos, en lugar de que los usuarios de la clase accedan directamente a los atributos. En Java, de manera general se implementan con atributos de instancia privados que tienen métodos públicos para recuperar o modificar los datos, comúnmente conocidos como getters y setters, respectivamente.

public class EjemploClase {
    private int datos;

    // Método getter
    public int getDatos() {
        return datos;
    }

    // Método setter
    public void setDatos(int nuevosDatos) {
        this.datos = nuevosDatos;
    }
}

Ningún elemento además de la propia clase debería tener acceso directo a sus datos.

Se dice que la clase encapsula los datos que contiene y evita que alguien acceda directamente a ellos. Con la encapsulación, una clase puede mantener ciertas invariantes sobre sus datos internos. Una invariante es una propiedad o verdad que se mantiene incluso después de que los datos son modificados. Por ejemplo, imaginemos que estamos diseñando una nueva clase Animal y tenemos los siguientes requisitos de diseño:

  • Cada animal tiene un campo de especie no nulo y no vacío.
  • Cada animal tiene un campo de edad que es mayor o igual a cero.

El objetivo al diseñar nuestra clase Animal sería asegurarnos de que nunca lleguemos a una instancia de Animal que viole una de estas propiedades. Al usar miembros de instancia privados junto con métodos getter y setter que validan los datos de entrada, podemos garantizar que estas invariantes sigan siendo verdaderas.

Por ejemplo, si definimos la clase Animal sin encapsulación:

public class Animal {
    public String especie;
    public int edad;
}

Al definir la clase Animal de esta manera, es fácil crear una instancia de Animal que no cumpla las invariantes:

Animal animal = new Animal(); // La especie no debería ser nula.
animal.edad = -80; // La edad no podría ser negativa.

L primera invariante se viola tan pronto como se crea el objeto, con la especie predeterminada a nula. Luego, el usuario establece el campo de edad en -80, ya que este campo es accesible públicamente, lo que resulta en la violación de la segunda invariante.

HAciendo los atributos privados, la clase es la única que puede modificar los datos directamente. Al definir constructores, getters y setters que cumplan las condiciones:

public class Animal {
    private String especie;
    private int edad;

    public Animal(String especie) {
        this.setEspecie(especie);
    }

    public String getEspecie() {
        return especie;
    }

    public void setEspecie(String especie) {
        if(especie == null || especie.trim().length()==0) {
            throw new IllegalArgumentException("La especie es obligatoria");
        }
        this.especie = especie;
    }

    public int getAge() {
        return edad;
    }

    public void setAge(int edad) {
        if(edad < 0) {
            throw new IllegalArgumentException("La edad no puede ser un número negativo");
        }
        this.edad = edad;
    }
}

Las variables especie y edad están marcadas como privadas, con métodos públicos getEspecie() y getEdad() para leer los datos. Además, setEspecie() y setEdad() ahora validan la entrada y lanzan una excepción si se viola una de nuestras invariantes. Además, se ha añadido un constructor no predeterminado que requiere un valor de especie y utiliza el método setter para validar la entrada.

La ventaja de esta nueva implementación de la clase Animal es que utiliza la encapsulación para hacer cumplir los principios de diseño de la clase. Cada vez que se pasa una instancia de un objeto Animal a un método, se puede usar sin requerir que se validen sus invariantes.

Bloqueo de acceso directo a atributos privados

EN la práctica, los getter o setter a menudo ofrece un acceso casi directo a los atributos privados:

private String nombre;

public String getNombre() {
    return nombre;
}

public void setNombre(String nombre) {
    this.nombre = nombre;
}

Aunque puede parecer una mala encapsulación, el campo nombre se puede cambiar sin aplicar ninguna regla, es mucho mejor que permitir el acceso directo a la variable privada nombre. La ventaja radica en que fácilmente se puede actualizar el método getter o setter para tener reglas más complejas sin hacer que las otras clases que lo usan tengan que recompilar el código. El método setNombre() podría reescribirse de la siguiente manera:

public void setNombre(String nombre) {
    this.nombre = (nombre == null || nombre.trim().length() == 0) ? null : nombre;
}

Dado que la firma del método setNombre() no cambió, la invocación de este método no implica tener que modificar y recompilar su código.

Se considera una buena práctica de diseño encapsular siempre todas las variables en una clase, incluso si no hay reglas de datos establecidas, como una forma de proteger los datos cuando dichas reglas puedan añadirse en el futuro.

Creación de JavaBeans

La encapsulación es tan prevalente en Java que existe un estándar para crear clases que almacenan datos, llamado JavaBeans. Un JavaBean es un principio de diseño para encapsular datos en un objeto en Java;

Convenciones de nomenclatura de JavaBean:

Regla Ejemplo
Las propiedades son privadas. private int edad;
El getter para propiedades no booleanas comienza con get. public int getEdad() { return edad; }
Los getters para propiedades booleanas pueden comenzar con is, has o get. public boolean isAve() { return ave; }
public boolean getAve() { return ave; }
Los métodos setters comienzan con set. public void setEdad(int edad) { this.edad = edad; }
El nombre del método debe tener un prefijo de set/get/is/has, seguido de la primera letra de la propiedad en mayúscula y el resto del nombre de la propiedad. public void setNumHijos(int numHijos) { this.numHijos = numHijos; }

Aunque los valores booleanos utilizan is para comenzar sus métodos getters, NO se aplica a las instancias de la clase envolvente Boolean, que utilizan get.

Por ejemplo:

private boolean jugando;
private Boolean bailando;

¿Cuál de las siguientes podría incluirse correctamente en un JavaBean?

public boolean isJugando() { return jugando; }
public boolean getJugando() { return jugando; }
public Boolean isBailando() { return bailando; }

La primera línea es correcta porque define un getter adecuado para una variable booleana. El segundo ejemplo también es correcto, ya que boolean puede usar is o get. La tercera línea es incorrecta, porque un envoltorio Boolean debería comenzar con get, ya que es un objeto.

public String nombre;
public String nombre() { return nombre; }
public void actualizarNombre(String n) { nombre = n; }
public void setnombre(String n) { nombre = n; }

¡Ninguna de estas líneas sigue las prácticas correctas de JavaBean! La primera línea hace público el nombre, cuando debería ser privado. La segunda línea no define un getter adecuado y debería ser getNombre(). Las dos últimas líneas son incorrectos setters, ya que la primera no comienza con set y la segunda no tiene la primera letra del nombre del atributo en mayúscula.

Relación Es‐un

El operador instanceof se puede usar para determinar cuándo un objeto es una instancia de una clase, superclase o interfaz particular. En el diseño orientado a objetos, se describe la relación de que un objeto es una instancia de un tipo de datos como tener una relación es‐un. La relación es‐un también se conoce como la prueba de herencia.

Cuando se construye un modelo de datos basado en la herencia, es importante aplicar la relación es‐un regularmente, para diseñar clases que conceptualmente tengan sentido. Por ejemplo, una clase Gato que hereda de una clase Mascota, como se muestra en la imagen:

classDiagram
    Mascota <|-- Gato

La clase principal, Mascota, tiene campos comunes como nombre y edad. Se podría incurrir en el error de diseñar una clase Tigre, y dado que los tigres también tienen una edad y un nombre, se podría estar tentado en reutilizar la clase principal Mascota con el propósito de ahorrar tiempo y líneas de código, dando lugar a diseños incorrectos:

classDiagram
    Mascota <|-- Gato
    Mascota <|-- Tigre

Por desgracias ;-), Mascota también tiene un método acariciar().
Al reutilizar la clase principal Mascota, se está afirmando conceptualmente que un Tigre es‐una Mascota, pero un Tigre no es una Mascota. Aunque este ejemplo es funcionalmente correcto y ahorra tiempo y líneas de código, el resultado de no aplicar la relación es‐un es que se ha creado una relación que viola el modelo de datos. Intentemos solucionar el problema colocando Mascota y Tigre debajo de una clase padre Felino y veamos si eso resuelve el problema:

classDiagram
    Felino <|-- Tigre
    Felino <|-- Mascota
    Mascota <|-- Gato
    Mascota <|-- Perro

La estructura de clases es ahora más coherente, pero si se añade un hijo Perro a Mascota, nos encontramos con un problema con la prueba es‐un. Un Perro es‐una Mascota, y una Mascota es‐una Felino, pero el modelo implica que un Perro es‐un Felino, lo cual obviamente no es cierto.

La prueba de relación es‐un ayuda a evitar crear modelos de objetos que contengan contradicciones. Una solución en este ejemplo sería no combinar Tigre y Mascota en el mismo modelo, prefiriendo escribir código duplicado en lugar de crear datos inconsistentes. Una mejor solución podría ser utilizar las propiedades de las interfaces y declarar Mascota como una interfaz en lugar de una clase principal:

classDiagram
    Animal <|-- Perro
    Animal <|-- Felino
    Felino <|-- Tigre
    Felino <|-- Gato
    Mascota <|.. Gato
    Mascota <|.. Perro

Ahora es correcto usando la prueba es‐un: Gato es‐un Animal, Tigre es‐un Felino, Perro es‐un Animal, y así sucesivamente. Mascota ahora está separada del modelo de herencia de clases, pero al usar interfaces, preservamos la relación de que Gato es‐una Mascota y Perro es‐una Mascota.

Relación Tiene‐un

En el diseño orientado a objetos, a menudo queremos probar si un objeto contiene una propiedad o valor en particular. La relación tiene‐un cuando la propiedad de una clase tiene un nombre de un objeto o primitiva como miembro. La relación tiene‐un también se conoce como la prueba de composición de objetos.

Por ejemplo, las clases Pájaro y Pico:

classDiagram
    Pato o-- Pico
    class Pico {
      -String color
      -double longitud
    }
    class Pato{
      -Pico pico
      -Pie pieDerecho
      -Pie pieIzquierdo
    }

Pájaro y Pico son clases con atributos y valores diferentes. Aunque obviamente fallan la prueba es‐un, ya que un Pájaro no es un Pico, ni un Pico es un Pájaro, sí pasan la prueba tiene‐un, ya que un Pájaro tiene un Pico. La herencia va un paso más allá al permitirnos decir que cualquier hijo de Pájaro también debe tener un Pico.

Problemas del modelo de datos empleando Es‐un y Tiene‐un

A veces, las relaciones parecen pasar la prueba es‐un pero fallan al combinarse con la relación tiene‐un a través de la herencia. Por ejemplo:

public class Cola {}
public class Primate {
    protected Cola cola;
}

public class Mono extends Primate { // El mono tiene una cola ya que es un primate
}
public class Chimpance extends Primate { // El chimpancé tiene una cola ya que es un primate
}

En el ejemplo, un Mono es‐un Primate y un Chimpance es‐un Primate. El modelo también establece que un Primate tiene‐una Cola, y mediante la herencia, un Mono tiene‐un Cola y un Chimpancé tiene‐un Cola. Sin embargo, los chimpancés no tienen cola, por lo que el modelo de datos subyacente es incorrecto.
Deberíamos eliminar la propiedad Cola de la clase Primates, ya que no todos los primates tienen colas.

Composición de Objetos

En el diseño orientado a objetos la composición de objetos es la propiedad de construir una clase utilizando referencias a otras clases para reutilizar la funcionalidad de esas clases.

La clase contiene las otras clases en el sentido de tiene‐un y puede delegar métodos a las otras clases.

La composición de objetos debe considerarse como una alternativa a la herencia y a menudo se utiliza para simular un comportamiento polimórfico que no se puede lograr mediante la herencia simple:

public class Aletas {
    public void aletear() {
        System.out.println("Las aletas se mueven de un lado a otro");
    }
}

public class PatasPalmeadas {
    public void patear() {
        System.out.println("Las patas palmeadas patean de un lado a otro");
    }
}

Intentar relacionar estos objetos mediante la herencia no tiene sentido, ya que las PatasPalmeadas no son lo mismo que las Aletas. En cambio, podemos crear una nueva clase que contenga ambas de estas clases y delegue sus métodos en ella:

public class Pingüino {
    private final Aletas aletas;
    private final PatasPalmeadas patasPalmeadas;

    public Pingüino() {
        this.aletas = new Aletas();
        this.patasPalmeadas = new PatasPalmeadas();
    }

    public void aletear() {
        this.aletas.aletear();
    }

    public void patear() {
        this.patasPalmeadas.patear();
    }
}

La clase Pingüino está compuesta por instancias de Aletas y PatasPalmeadas. La parte difícil de aletear() y patear() se delega a las otras clases, siendo los métodos en la clase Pingüino de solo una línea. Ten en cuenta que las implementaciones de estos métodos en las clases delegadas también tienen solo una línea, aunque podrían ser considerablemente más complejas.

Una de las ventajas de la composición de objetos sobre la herencia es que tiende a promover una mayor reutilización de código. Al utilizar la composición de objetos, se accede a otras clases y métodos que serían difíciles de obtener mediante el modelo de herencia simple.

En el ejemplo, la clase Aletas se puede reutilizar en clases completamente no relacionadas con un Pingüino o un Ave, como en una clase Delfín o Tortuga. Si la clase Aletas hubiera sido heredada de la clase Pingüino, entonces usarla en otras clases no relacionadas sería difícil sin romper el modelo de clases o tener que hacer que la otra clase contenga una instancia de un Pingüino. Por ejemplo, sería absurdo decir que un Delfín hereda de un Pingüino o tiene una instancia de una clase Pingüino, sólo porque un Delfín tiene Aletas y Aletas hereda de la clase Pingüino.

Por otro lado, sobrecarga de métodos para determinar dinámicamente qué método seleccionar en tiempo de ejecución es una herramienta muy útil.

Última actualización: 23.09.2025

02. Patrón Singleton


1. El Patrón Singleton

En muchos casos, precisamos crear un objeto en memoria sólo una vez en una aplicación y compartirlo entre varias clases. Un ejemplo podría ser una conexión a una base de datos, pero sucede con frecuenta con otro tipo de recursos compartidos.

Por ejemplo, podríamos querer gestionar la cantidad de comida disponible para la alimentación de los animales de un zoológico en todas las clases que lo utilizan. Podríamos pasar el mismo objeto compartido ComidaManager a cada clase y método que lo utiliza, aunque esto crearía muchos punteros adicionales y podría ser difícil de gestionar si el objeto se utiliza en toda la aplicación. Al crear un objeto singleton ComidaManager, centralizamos los datos y eliminamos la necesidad de pasarlos por toda la aplicación.

  • El patrón singleton es un patrón de creación que permite sólo una instancia de un objeto en memoria dentro de una aplicación, compartida por todas las clases y hilos dentro de la aplicación.

  • El objeto disponible globalmente creado por el patrón singleton se denomina objeto singleton.

  • Los singleton también pueden mejorar el rendimiento cargando datos reutilizables que de otro modo serían costosos de almacenar y recargar cada vez que se necesitan.

Una implementación sencilla de clase ComidaManager con un patrón singleton básico:

public class AlmacenComida {

    private int cantidad = 0;

    private AlmacenComida() {} // Privado, se crea como atributo estático:

    private static final AlmacenComida instancia = new AlmacenComida(); // static: único.

    // Método público único para obtener la instancia:
    public static AlmacenComida getInstance() {
        return instancia;
    }

    public synchronized void addComida(int cantidad) {
        this.cantidad += cantidad;
    }

    public synchronized boolean removeComida(int cantidad) {
        if (this.cantidad < cantidad) return false;
        this.cantidad -= cantidad;
        return true;
    }

    public synchronized int getCantidadComida() {
        return cantidad;
    }
}
  • Los singleton en Java se crean como atributos privados estáticos dentro de la clase, a menudo con el nombre instance (o instancia).
  • Se accede a ellos a través de un único método público estático, a menudo llamado getInstance(), que devuelve la referencia al objeto singleton.
  • Todos los constructores en una clase singleton se declaran como privados, lo que garantiza que ninguna otra clase pueda instanciar otra versión de la clase. Al marcar los constructores como privados, hemos marcado implícitamente la clase como final.

Recuerda que cada clase requiere al menos un constructor, con el constructor por defecto si no se proporciona ninguno. Además, la primera línea de cualquier constructor es una llamada a un constructor padre super(). Si todos los constructores se declaran privados en la clase singleton, entonces es imposible crear una subclase con un constructor válido; por lo tanto, la clase singleton es final.

  • Se añade el modificador synchronized a addComida(), removeComida() y getCantidadComida(), para evitar que dos procesos ejecuten el mismo método exactamente al mismo tiempo y haya inconsistencias.

En el ejemplo AlmacenComida, un proceso que quiera usar este singleton primero llama a getInstance() y luego llama al método público necesario:

public class EntrenadorLlamas {

    public boolean alimentarLlamas(int numeroLlamas) {

        int cantidadNecesaria = 5 * numeroLlamas;

        AlmacenComida AlmacenComida = AlmacenComida.getInstance();

        if (AlmacenComida.getCantidadComida() < cantidadNecesaria) {
            AlmacenComida.addComida(cantidadNecesaria + 10);
        }

        boolean alimentadas = AlmacenComida.removeComida(cantidadNecesaria);

        if (alimentadas)
            System.out.println("Las llamas han sido alimentadas");
        return alimentadas;
    }

}

Un aspecto a tener en cuenta es que puede haber varias clases en la aplicación que requieran el objeto Singleton.
En el ejemplo, equivaldría a muchas instancias de EntrenadorLlamas pero sólo una instancia del objeto Singleton, AlmacenComida.
El boolean devuelto por removeComida(), permite comprobar si hay disponible.
En nuestro primer ejemplo de AlmacenComida, instanciamos el objeto singleton directamente en la definición de la referencia de instancia.

2. Tipos de implementación del patrón Singleton

Se puede instanciar un singleton de varios modos:

a) En la declaración del objeto:

private static final ClaseSingleton instancia = new ClaseSingleton();

b) Utilizando un bloque de inicialización estático:

    private static final ClaseSingleton instancia;

    static {
        instancia = new ClaseSingleton();
        // Realizar pasos adicionales
    }

c) Instanciación “perezosa” de Singleton. Se realiza en la primera llamada a la invocación del método getInstance:

    private static ClaseSingleton instancia;

    private ClaseSingleton() {
    }

    public static ClaseSingleton getInstance() {
        if (instancia == null) {
            instancia = new ClaseSingleton(); // ¡NO ES SEGURO PARA HILOS!
        }
        return instancia;
    }

Por ejemplo:

// Instanciación usando un bloque estático
public class RegistroPersonal {
    private static final RegistroPersonal instancia;

    static {
        instancia = new RegistroPersonal();
        // Realizar pasos adicionales
    }

    private RegistroPersonal() {
    }

    public static RegistroPersonal getInstance() {
        return instancia;
    }

    // Métodos de acceso a datos
    // ...
}

Tanto la clase RegistroPersonal como la clase anterior AlmacenComida instancian el singleton en el momento en que se carga la clase. Sin embargo, a diferencia de la clase AlmacenComida, la clase RegistroPersonal instancía el singleton como parte de un bloque de inicialización estático.

Aunque estas dos implementaciones son equivalentes, ya que ambas crean el singleton cuando se carga la clase, el bloque de inicialización estático permite realizar pasos adicionales para configurar el singleton después de que se ha creado. También permite gestionar casos en los que el constructor de RegistroPersonal lanza una excepción.
Dado que el singleton se crea cuando se carga la clase, permite marcar la como final, lo que garantiza que sólo se creará una instancia dentro de la aplicación.

Cuándo usar singleton

Los singletons se utilizan en situaciones donde necesitamos acceso a un conjunto único de datos en toda una aplicación. Por ejemplo: datos de configuración de la aplicación o las cachés de datos reutilizables se implementan comúnmente mediante singletons. Los singletons también se pueden utilizar para coordinar el acceso a recursos compartidos, como coordinar el acceso de escritura a un archivo o acceso a bases de datos.

Hay muchas formas de hacer esto en Java. Todas estas formas difieren en su implementación del patrón, pero al final, todas logran el mismo resultado final de una única instancia.

1. Inicialización en la decleración

Es el método más sencillo de crear una clase singleton. El objeto de la clase se crea cuando se carga en la memoria por la JVM, asignando directamente la referencia de una instancia.

Puede utilizarse cuando el programa siempre usará una instancia de esta clase, o el costo de crear la instancia no es demasiado grande en términos de recursos y tiempo.

// Código Java para crear una clase singleton mediante
// "Inicialización Ansiosa"
public class ClaseSingletonAD {
  // instancia pública inicializada al cargar la clase
  private static final ClaseSingletonAD instance = new ClaseSingletonAD();
 
  private ClaseSingletonAD() {
    // constructor privado
  }
  
  public static ClaseSingletonAD getInstance(){
    return instance;
  }
}

Ventajas/inconvenientes:

  • Muy sencilla de implementar.
  • Puede llevar al desperdicio de recursos, ya que la instancia de la clase se crea siempre, ya sea que se necesite o no.
  • También se pierde tiempo de CPU en la creación de la instancia si no es necesaria.
  • No es posible manejar excepciones.

2. Usando bloques static

Es muy similar al caso anterior. La única diferencia es que el objeto se crea en un bloque estático para que se pueda tener acceso a su creación, como el manejo de excepciones. De esta manera, el objeto también se crea en el momento de la carga de la clase.

Puede utilizarse cuando hay posibilidad de excepciones al crear el objeto con inicialización ansiosa.

// Código Java para crear una clase singleton
// Usando bloque estático
public class ClaseSingletonAD {
  // instancia pública
  public static ClaseSingletonAD instance;
 
  private ClaseSingletonAD() {
    // constructor privado
  }
  
  static {
    // bloque estático para inicializar la instancia
    instance = new ClaseSingletonAD();
  }
}

Ventanjas e inconvenientes:

  • Muy sencillo de implementar.
  • No es necesario implementar el método getInstance(). La instancia se puede acceder directamente.
  • Las excepciones se pueden manejar en el bloque estático.
  • Puede llevar al desperdicio de recursos, ya que la instancia de la clase se crea siempre, ya sea que se necesite o no.
  • También se pierde tiempo de CPU en la creación de la instancia si no es necesaria.

3. Instanciación “perezosa” de Singleton

En este método, el objeto se crea sólo si es necesario. Esto puede evitar el desperdicio de recursos.

El objeto se crea en la implementación del método getInstance() que devuelva la instancia. Hay una comprobación de nulidad, y si el objeto no está creado, entonces se crea; de lo contrario, se devuelve el creado previamente.

Dado que el objeto se crea dentro de un método, se garantiza que el objeto no se creará a menos que sea necesario. La instancia se mantiene privada para que nadie pueda acceder directamente a ella.

Puede utilizarse en un entorno de un solo hilo porque varios hilos pueden romper la propiedad singleton al acceder al método getInstance() simultáneamente y crear varios objetos.

// inicialización perezosa (Lazy initialization)
public class ClaseSingletonAD {
  // instancia privada
  // sólo accesible desde el méttodo getInstance()
  private static ClaseSingletonAD instance;
 
  private ClaseSingletonAD() {
    // constructor privado
  }
 
  //devuelve la instancia de la clase
  public static ClaseSingletonAD getInstance() {
    if (instance == null) {
      // si la instancia es nula se inicializa
      instance = new ClaseSingletonAD();
    }
    return instance;
  }
}

Por ejemplo:

// Instanciación perezosa
public class RastreadorTicketsVisitantes {
    private static RastreadorTicketsVisitantes instancia;

    private RastreadorTicketsVisitantes() {
    }

    public static RastreadorTicketsVisitantes getInstance() {
        if (instancia == null) {
            instancia = new RastreadorTicketsVisitantes(); // ¡NO ES SEGURO PARA HILOS!
        }
        return instancia;
    }

    // Métodos de acceso a datos
    // ...
}

El RastreadorTicketsVisitantes, al igual que nuestras clases singleton, declara sólo constructores privados, crea una instancia de singleton y devuelve el singleton con un método getInstance(). Sin embargo, la clase RastreadorTicketsVisitantes crea el objeto singleton primera vez que un cliente lo solicita. Crear un objeto reutilizable la primera vez que se solicita es un patrón de diseño de software conocido como instanciación perezosa (Lazy Instantiation), que se usa a menudo junto con el patrón singleton.

Ventajas: La instanciación perezosa reduce el uso de memoria y mejora el rendimiento cuando se inicia una aplicación. De hecho, sin instanciación perezosa, la mayoría de los sistemas operativos y aplicaciones que ejecutas tardarían significativamente más en cargarse y consumirían mucha más memoria, quizás más memoria de la disponible en tu computadora.

Inconveniente:" El inconveniente de la instanciación perezosa es que los usuarios pueden notar un retraso notable la primera vez que se necesita un tipo particular de recurso, como una conexión a una base de datos.

Por ejemplo, muchas herramientas libres IDEs de desarrollo, como Eclipse, a menudo muestran un ligero retraso la primera vez que se abre un archivo Java en una ventana del editor después de iniciar el programa. Sin embargo, el retraso desaparece cuando se abren archivos Java adicionales. Este es un ejemplo de instanciación perezosa, ya que Eclipse sólo carga las bibliotecas para analizar y presentar archivos Java la primera vez que se abre un archivo Java.

  • El objeto se crea sólo si es necesario. Puede superar el desperdicio de recursos y tiempo de CPU.
  • El manejo de excepciones también es posible en el método.
  • Se debe verificar la condición de nulo cada vez.
  • La instancia no se puede acceder directamente.
  • En un entorno multithread, puede romper la propiedad singleton.

Singleton en varias computadoras

En principio, los singletons son siempre únicos. Cuando se escriben aplicaciones que se ejecutan en varias computadoras, la solución estática de singleton comienza a requerir consideraciones especiales, ya que cada computadora tendría su propio JVM. En esas situaciones, aún se podría usar el patrón singleton, aunque podría implementarse con una base de datos o un servidor de colas en lugar de como un objeto estático.

4. Thread Safe Singleton

  • Implementar verdaderamente el patrón singleton, debemos asegurarnos de que sólo se cree una instancia del singleton y está garantizado con el modelo anterior en entornos con un úncio hilo de ejecución.

  • Marcar el constructor como privado evita que el singleton sea creado por otras clases, pero también se necesita asegurar que el objeto singleton sólo se crea una vez dentro de la propia clase singleton.

Se garantiza en los ejemplos de las clases AlmacenComida y RegistroPersonal utilizando el modificador final en la referencia estática.

Pero, en la instanciación perezosa en la clase RastreadorTicketsVisitantes, el compilador no permite asignar el modificador final a la referencia estática.

  • La implementación de RastreadorTicketsVisitantes, como se muestra, no se considera segura para hilos de ejecución (Threads), ya que dos hilos podrían llamar a getInstance() al mismo tiempo, lo que daría lugar a la creación dos objetos. Después de que ambos hilos terminen de ejecutarse, sólo se establecerá y utilizará un objeto por otros hilos en adelante, pero el objeto que recibieron los dos hilos iniciales puede no ser el mismo.

La seguridad de hilos (Thread safety) es la propiedad de un objeto que **garantiza una ejecución segura por parte de múltiples hilos al mismo tiempo mediante el uso del modificador synchronized** (por ejemplo, es la diferencia entre StringBuilder y StringBuffer). Hace que el método getInstance() sea sincronizado para que múltiples hilos no puedan acceder a él simultáneamente.

public static synchronized RastreadorTicketsVisitantes getInstance() {
    if (instancia == null) {
        instancia = new RastreadorTicketsVisitantes();
    }
    return instancia;
}

El método getInstance() ahora está sincronizado, lo que significa que sólo se permitirá que un hilo entre en el método a la vez, asegurando que sólo se cree un objeto.

// Programa Java para crear una clase Singleton
// segura para subprocesos
public class ClaseSingletonAD 
{
  // instancia privada, para que sólo pueda ser
  // accedida por el método getInstance()
  private static ClaseSingletonAD instance;
 
  private ClaseSingletonAD() {
    // constructor privado
  }
 
  // método sincronizado para controlar el acceso simultáneo
  synchronized public static ClaseSingletonAD getInstance() {
    if (instance == null) {
      // si la instancia es nula, inicializar
      instance = new ClaseSingletonAD();
    }
    return instance;
  }
}

Ventajas e inconvenientes:

  • La inicialización perezosa se implanta.
  • También es seguro para threads.
  • El método getInstance() está sincronizado, por lo que provoca un rendimiento lento, ya que varios hilos no pueden acceder a él simultáneamente.

5. Singletons con bloqueo de doble comprobación (Double‐Checked Locking)

La implementación sincronizada de getInstance(), aunque evita correctamente la creación de varios objetos singleton, tiene el problema de que cada llamada a este método requerirá sincronización. En la práctica, esto puede ser costoso y afectar el rendimiento.

La sincronización sólo es necesaria la primera vez que se crea el objeto. La solución es utilizar el doble bloqueo, un patrón de diseño en el que primero probamos si se necesita sincronización antes de adquirir cualquier bloqueo:

private static volatile ClaseSingleton instance;

public static ClaseSingleton getInstance() {
    if (instance == null) {
        synchronized (ClaseSingleton.class) {
            if (instance == null) {
                instance = new ClaseSingleton();
            }
        }
    }
    return instance;
}

Se añade el modificador volatile al objeto singleton. Esta palabra clave evita un caso sutil donde el compilador intenta optimizar el código de manera que el objeto se acceda antes de que termine de construirse.

Esta solución es mejor que la versión anterior, ya que realiza el paso de sincronización sólo cuando el singleton no existe. El singleton se accede miles de veces durante muchas horas o días, esto significa que sólo las primeras llamadas requerirían sincronización, y el resto no.

  • Realiza inicialización perezosa.
  • Es thread-safe
  • Mejora el rendimiento, porque reduce la sincronización.
  • La primera vez puede afectar al rendimiento.

Conclusión

Singleton es un patrón de diseño útil para permitir sólo una instancia de su clase, pero los errores comunes pueden permitir que se cree más de una instancia sin darse cuenta.

El propósito del Singleton es controlar la creación de objetos, limitando el número a uno pero permitiendo la flexibilidad de crear más objetos si la situación cambia. Dado que sólo hay una instancia de Singleton, cualquier campo de instancia de un Singleton aparecerá sólo una vez por clase, al igual que los campos estáticos.

Los singleton a menudo controlan el acceso a recursos como conexiones de bases de datos o sockets. Por ejemplo, si se tiene una licencia para una sola conexión para su base de datos o su controlador JDBC tiene problemas con subprocesos múltiples, Singleton se asegura de que sólo se realice una conexión o que sólo un subproceso pueda acceder a la conexión a la vez. Si añaden conexiones de base de datos o se utiliza un controlador JDBC que permite subprocesos múltiples, Singleton se puede ajustar fácilmente para permitir más conexiones.

Además, los Singleton pueden tener estado; en este caso, su función es servir como depósito único del Estado. Si está implementando un contador que necesita dar números secuenciales y únicos (como la máquina que da números en la tienda de delicatessen), el contador debe ser globalmente único. El Singleton puede retener el número y sincronizar el acceso; Si más adelante desea mantener contadores en una base de datos para lograr persistencia, puede cambiar la implementación privada de Singleton sin cambiar la interfaz.

Por otro lado, los Singleton también pueden proporcionar funciones de utilidad que no necesitan más información que sus parámetros. En ese caso, no hay necesidad de crear instancias de múltiples objetos que no tienen ninguna razón para su existencia, por lo que un Singleton es apropiado.

Última actualización: 23.09.2025

03. Objetos Inmutables


1. Creación de Objetos Inmutables

El próximo patrón creacional que discutiremos es el patrón de objetos inmutables, empelado para cuando deseamos tener objetos de sólo lectura para que sean compartidos y utilizados por varias clases.

A veces queremos crear objetos simples que puedan ser compartidos entre varias clases, pero por razones de seguridad no queremos que su valor sea modificado. Podríamos copiar el objeto antes de enviarlo a otro método, pero esto crea una gran sobrecarga que duplica el objeto cada vez que se pasa. Además, si tenemos varios hilos que acceden al mismo objeto, podríamos tener problemas de concurrencia, como veremos en el Capítulo 7.

Solución: El patrón de objeto inmutable es un patrón creacional basado en la idea de crear objetos cuyo estado no cambia después de que se crean y que se pueden compartir fácilmente entre varias clases. Los objetos inmutables van de la mano con la encapsulación, excepto que no existen métodos setter que modifiquen el objeto. Dado que el estado de un objeto inmutable nunca cambia, son inherentemente seguros para subprocesos.

Aplicación de una Estrategia Inmutable

Aunque hay una variedad de técnicas para escribir una clase inmutable, debes estar familiarizado con una estrategia común para hacer que una clase sea inmutable para el examen:

  1. Usa un constructor para establecer todas las propiedades del objeto.
  2. Marca todas las variables de instancia como privadas y finales.
  3. No definas ningún método setter.
  4. No permitas que los objetos mutables referenciados se modifiquen o accedan directamente.
  5. Evita que los métodos se anulen.

La primera regla define cómo creamos el objeto inmutable, pasando la información al constructor para que todos los datos se establezcan al crearlo. Las segundas y terceras reglas son directas, ya que provienen de la encapsulación adecuada. Si las variables de instancia son privadas y finales, y no hay métodos setter, entonces no hay una forma directa de cambiar la propiedad de un objeto. Todas las referencias y valores primitivos contenidos en el objeto se establecen en la creación y no se pueden modificar.

La cuarta regla requiere una explicación más detallada. Supongamos que tienes un objeto inmutable Animal, que contiene una referencia a una lista de alimentos favoritos del animal, como se muestra en el siguiente ejemplo:

import java.util.*;

public final class Animal {
    private final List<String> favoriteFoods;

    public Animal(List<String> favoriteFoods) {
        if (favoriteFoods == null) {
            throw new RuntimeException("favoriteFoods is required");
        }
        this.favoriteFoods = new ArrayList<String>(favoriteFoods);
    }

    public List<String> getFavoriteFoods() { // ¡HACE LA CLASE MUTABLE!
        return favoriteFoods;
    }
}

Para asegurarnos de que la lista favoriteFoods no sea nula, la validamos en el constructor y lanzamos una excepción si no se proporciona. El problema en este ejemplo es que el usuario tiene acceso directo a la lista definida en nuestra instancia de Animal. Aunque no pueden cambiar el objeto List al que apunta, pueden modificar los elementos de la lista, por ejemplo, eliminando todos los elementos llamando a getFavoriteFoods().clear(). También podrían reemplazar, eliminar o incluso ordenar la lista.

La solución, entonces, es nunca devolver esa referencia de lista al usuario. De manera más general, nunca deb

es compartir referencias a un objeto mutable contenido dentro de un objeto inmutable. Si el usuario necesita acceso a los datos en la lista, ya sea creando métodos envolventes para iterar sobre los datos o creando una copia única de los datos que se devuelve al usuario y nunca se almacena como parte del objeto. De hecho, la API de Collections incluye el método Collections.unmodifiableList(), que hace exactamente esto.

La clave aquí es que ninguno de los métodos que creas debe modificar el objeto mutable.

Volviendo a nuestras cinco reglas, la última regla es importante porque evita que alguien cree una subclase de tu clase en la que un valor previamente inmutable ahora parece mutable. Por ejemplo, podrían anular un método que modifica una variable diferente en la subclase, ocultando essencialmente la variable privada definida en la clase principal. La solución más simple es marcar la clase o los métodos con el modificador final, aunque esto limita el uso de la clase. Otra opción es hacer que el constructor sea privado y aplicar el patrón de fábrica, que discutiremos más adelante en este capítulo.

Una clase Animal inmutable:

import java.util.*;

public final class Animal {
    private final String species;
    private final int age;
    private final List<String> favoriteFoods;

    public Animal(String species, int age, List<String> favoriteFoods) {
        this.species = species;
        this.age = age;
        if (favoriteFoods == null) {
            throw new RuntimeException("favoriteFoods is required");
        }
        this.favoriteFoods = new ArrayList<String>(favoriteFoods);
    }

    public String getSpecies() {
        return species;
    }

    public int getAge() {
        return age;
    }

    public int getFavoriteFoodsCount() {
        return favoriteFoods.size();
    }

    public String getFavoriteFood(int index) {
        return favoriteFoods.get(index);
    }
}

¿Sigue este ejemplo las cinco reglas? Bueno, todos los campos están marcados como privados y finales, y el constructor los establece al crear el objeto. Luego, no hay métodos setter y la clase en sí está marcada como final, por lo que los métodos no pueden ser anulados por una subclase. La clase contiene un objeto mutable, List, pero no hay referencias al objeto disponibles públicamente. Proporcionamos dos métodos para recuperar el número total de alimentos favoritos, así como un método para recuperar un alimento basado en un valor de índice. Ten en cuenta que String se considera inmutable, por lo que no tenemos que preocuparnos de que los objetos String se modifiquen. Por lo tanto, se cumplen las cinco reglas y las instancias de esta clase son inmutables.

Manejo de Objetos Mutables en los Constructores de Objetos Inmutables

Puedes notar que creamos un nuevo ArrayList en el constructor de Animal. Esto es absolutamente importante para evitar que la clase que crea inicialmente el objeto mantenga una referencia a la List mutable utilizada por Animal. Considera si hubiéramos hecho lo siguiente en el constructor:

this.favoriteFoods = favoriteFoods;

Con este cambio, el llamador que crea el objeto está utilizando la misma referencia que el objeto inmutable, lo que significa que ¡tiene la capacidad de cambiar la List! Es importante, al crear objetos inmutables, copiar cualquier argumento de entrada mutable a la instancia en lugar de usarlo directamente.

“Modificación” de un Objeto Inmutable

¿Cómo modificamos objetos inmutables si son inherentemente inmodificables? La respuesta es que ¡no podemos! Alternativamente, podemos crear nuevos objetos inmutables que contengan toda la misma información que el objeto original más lo que queríamos cambiar. Esto sucede cada vez que combinamos dos cadenas:

String firstName = "Grace";
String fullName = firstName + " Hopper";

En este ejemplo, firstName es inmutable y no se modifica al agregarse a fullName, que también es un objeto inmutable. También podemos hacer lo mismo con nuestra clase Animal. Imagina que queremos aumentar la edad de un Animal en uno. Lo siguiente crea dos instancias de Animal, la segunda utilizando una copia de los datos de la primera instancia:

// Crea una nueva instancia de Animal
Animal leon = new Animal("león", 5, Arrays.asList("carne", "más carne"));
// Crea una nueva instancia de Animal usando datos de la primera instancia
List<String> alimentosFavoritos = new ArrayList<String>();
for (int i = 0; i < leon.getFavoriteFoodsCount(); i++) {
    alimentosFavoritos.add(leon.getFavoriteFood(i));
}
Animal leonActualizado = new Animal(leon.getSpecies(), leon.getAge() + 1, alimentosFavoritos);

Dado que no teníamos acceso directo a la List mutable favoriteFoods, tuvimos que copiarla utilizando los métodos disponibles en la clase inmutable. También podríamos simplificar esto definiendo un método en Animal que devuelva una copia de la List de alimentos favoritos, siempre que el llamador comprenda que modificar esta List copiada no cambia el objeto Animal original de ninguna manera.

Última actualización: 23.09.2025

05. Programación en Kotlin

Abordaremos contenidos básicos de la programación estructurada, orientada a objetos y funcional. No son contenidos muy detallados, sólo se alcanza el nivel de detalle suficiente para abordar los primeros retos algorítmicos.

Última actualización: 23.09.2025

Subsecciones de 05. Programación en Kotlin

Programación estructurada básica

Abordaremos los contenidos más básicos de programación, que se corresponden con lo que se conoce como “programación estructurada” utilizando el lenguaje de programación Kotlin. No son contenidos muy detallados, sólo se alcanza el nivel de detalle suficiente para abordar los primeros retos algorítmicos.

Última actualización: 23.09.2025

Subsecciones de Programación estructurada básica

La funcion main()

Qué es una función

Una función se puede definir informalmente como un bloque de código diseñado para realizar una tarea en particular. En Kotlin todas las funciones comienzan a escribirse con la palabra reservada fun

La función main()

Toda aplicación necesita un punto de entrada, es decir, un sitio por de comenzar la ejecución. Para nuestras aplicaciones de consola el punto de entrada será la función main(). Todos nuestros programas tiene que tener escrita obligatoriamente esta función.

Nuestro primer ejemplo:

fun main() {
   println("hola mundo!")
}

Intuye la forma de escribir una función en Kotlin:

  • comienza con la palabra reservada fun
  • le sigue el nombre de la función, main en este caso
  • paréntesis que pueden estar vacios o con parámetros como veremos a continuación
  • un cuerpo de la función que va entre las llaves {}
  • dentro de las llaves irán las instrucciones o código de la función. en este otra función que imprime un mensaje

La función main() con parámetros

La función main() a menudo la veremos escrita con parámetros. El funcionamiento de los parámetros es bastante similar a los parámetros de una función matemática. Iremos viendo el funcionamiento de los parámetros en las funciones poco a poco.

fun main(args: Array<String>) {
   println("hola mundo!")
}

Por el momento para nuestros primeros ejemplos sencillos no son necesarios parámetros y escribiremos la función main sin parámetros como en el primer ejemplo.

Sobre el término compilador

Verás este termino con precisión y formalidad más adelante. Es una palabra que se utiliza frecuentemente en las explicaciones para aprender a programar. Para salir del paso, decimos que el compilador es una herramienta(programa) de un lenguaje de programación que se encarga de revisar que las instrucciones que escribimos son correctas según las normas del lenguaje y a continuación lo traduce a un lenguaje de bajo nivel (0s y 1s) para ser ejecutado en el procesador. Frecuentemente diremos que algo “da error de compilación” para indicar que algo está mal escrito y no vamos poder ejecutar el programa hasta que corrigamos el error. En el siguiente ejemplo nos olvidamos del paréntesis de cierre. Cuando intentamos ejecutar el programa el compilador nos da un error y el programa no llega a ejecutarse.

fun main(args: Array<String>) {
   println("hola mundo!"
}
Última actualización: 23.09.2025

La funcion print() y println()

Fíjate que ya usamos en nuestro primer ejemplo la función println(). A continuación aprenderemos alguna cosa más sobre esta imortante función y de paso sobre las funciones Kotlin en general.

El término salida estándar

El término salida se refiere al dispositivo en el que un programa escribe algo como por ejemplo un disco, una impresora o la pantalla del ordenador. El término salida estandar se refiere a la salida que el programa usa por defecto. Normalmente la salida estandar es la pantalla (monitor) así que a efectos prácticos, por el momento, puedes considerar monitor y salida estándar como sinónimos.

Argumentos de una función

Sin el menor rigor, teniendo en cuenta que lo explicaremos más adelante detalladamente intenta entender lo que es un argumento: Cuando usas una función ya definida y la llamas o invocas lo que escribes entre paréntesis son los argumentos. Internamente, la función usará esos argumentos o valores para hacer algo. Por ejemplo en

println("hola mundo")

“hola mundo” es un argumento. La función toma el argumento que le indicamos y es capaz de imprimir ese argumento en la pantalla.

Diferencia entre print() y println()

print() es una función en Kotlin que imprime su argumento en la salida estándar. De manera similar, println() es otra función que imprime su argumento en la salida estándar pero también agrega un salto de línea en la salida. Puedes probar el siguiente ejemplo para observar la diferencia

fun main(){
    println("Hello,")
    println(" world!")

    print("Hello,")
    print(" world!")
}

Al ejecutar generará el siguiente resultado

Hello,
world!
Hello, world!

Ambas funciones, print() y println(), se pueden usar para imprimir números y cadenas pero también se pueden pasar como argumentos expresiones que pueden consistir en un cálculo matemático o una concatenación de cadenas de caracteres entre otras muchas posibilidades.

fun main(){
    println( 200 )
    println( "200" )
    println( 2 + 2 )
    println("hola" + "y"+"adios")
    println(4*3)
}

Al ejecutar generará el siguiente resultado

200
200
4
holayadios
12

El detalle de que expresiones admiten estas funciones se irá viendo poco a poco. Por el momento basta con que aprecies que a las indicaciones dadas.

Última actualización: 23.09.2025

Comentarios

Un comentario es una explicación que el programador deja entre el código que escribe. Dicho comentario a la persona que lee el el código pero es ignorado por el compilador. Se usan comentarios para:

  • dejar explicaciones extra para cuando otro programador lea el código
  • dejar cualquier aclaración o recordatorio para el propio autor del programa.

Al igual que la mayoría de los lenguajes modernos, Kotlin admite comentarios de una sola línea (o de final de línea) y de varias líneas (bloque).

Comentarios de una sola línea

Los comentarios de una sola línea en Kotlin comienzan con dos barras diagonales // y terminan con el final de la línea. Por lo tanto , el compilador de Kotlin ignora cualquier texto escrito entre // y el final de la línea.

El siguiente es el programa Kotlin de muestra que utiliza un comentario de una sola línea

// esto es un comentario

fun main() {
    println("Hello, World!")
}

Cuando ejecute el programa Kotlin anterior, generará el siguiente resultado:

Hello, World!

Es decir, el comentario fue ignorado por el compilador. No tuvo ninguna influencia en la ejecución del programa.

Un comentario de una sola línea puede comenzar desde cualquier parte del programa y finalizará hasta el final de la línea. Por ejemplo, puede usar un comentario de una sola línea de la siguiente manera:

fun main() {
    println("Hello, World!") // Esto también es un comentario
}

Comentarios multilínea

Comienza con /* y termina con */ . Por lo tanto, cualquier texto escrito entre /* y */ se tratará como un comentario y el compilador de Kotlin lo ignorará.

Los comentarios de varias líneas también se denominan comentarios de bloque en Kotlin.

El siguiente es el programa Kotlin de muestra que utiliza un comentario de varias líneas:

/* Esto es un comentario multiLinea
 puede escribrise o extenderse
 en tantas líneas como tu quieras
 */

fun main() {
    println("Hello, World!")
}

Aunque no es necesario, es típico y una buena práctica, al escribir comentarios multilínea, añadir un * a cada linea que forma parte del comentario. Esto hace, especialmente en comentarios largos, que no confundamos lineas de comentario con lineas de código.

/* Esto es un comentario multiLinea
* puede escribrise o extenderse
* en tantas líneas como tu quieras
 */

fun main() {
    println("Hello, World!")
}
Última actualización: 23.09.2025

Variables, literales y sentencia de asignación

De forma informal y como punto de partida, llamamos variables a las ubicaciones de la memoria del ordenador que se usan para almacenar valores en un programa. Luego, a lo largo del programa podemos usa esos nombres para recuperar los valores almacenados y usarlos en el programa. Si tengo una variable x que almacena el valor 5 puedo decir indistintamente que x almacena 5 o simplemente que x vale 5.

A lo largo del curso afinaremos el concepto de variable pero con esta idea inicial informal es suficiente para despegar.

Crear variables

Hay varias posibilidades nos centramos en la más habitual.

Las variables de Kotlin se crean usando las palabras var o val y luego un signo igual = para asignar un valor a esas variables. En el siguiente ejemplo creamos una variable con var y otra con val

var nombre = "Juan Perro"
val edad = 15

Más adelante en este documento explicaremos su diferencia.

Sentencia de asignación

Una sentencia de asignación consiste en una sentencia con la siguiente estructura

nombreDeVar = valor o expresión

Cuando declaramos en los ejemplos anteriores una variable estabamos realmente usando una sentencia de asignación despues de la palabra reservada var. Una vez que creo una variable, si es mutable (concepto que vemos más adelante), puedo modificar su valor con todas las instrucciones de asignación que quiera.

fun main() {
    var numero = 3
    println(numero)
    numero = 7 //ahora la variable número almacena el valor 7
    println(numero)
    numero =99  //ahora la variable número almacena el valor 99
    println(numero)
}

Orden de acciones en la sentencia de asignación

Utilizando el operador de asignación “=” es posible que una variable modifique su valor en función de su valor actual

fun main() {
    var x=3
    var y=7
    println(x)
    x= 5+1// se calcula 5+1 y se asigna el resultado a x
    println(x)
    x= y+4 //se lee  el valor de y, se le suma 4  y el resultado se asinga a x
    println(x)
}

Veamos ahora una sentencia de asignación en la que la misma variable aparece a la izquierda y derecha del igual. Es algo muy habitual y tienes que entenderlo sin titubeos.

fun main() {
    var numero=3
    numero=numero +1
    println(numero)
}

El ejemplo anterior imprime un 4. Para entender sentencias como

 numero=numero +1

simplemente tienes que tener claro el funcionamiento general de la sentencia de asignación:

  1. Se evalúa (calcula) la parte derecha
  2. Se asigna dicho valor a la variable de la izquierda

Es muy importante que tengas en cuenta que los pasos de arriba ocurren en secuencia, primero se ejecuta el paso 1 y a continuación el paso 2. De esta manera es sencillo el razonamiento de

 numero=numero +1
  1. Al calcular el valor de la derecha ocurre que numero almacena el valor 3. Por tanto se calcula “3+1” que vale 4.
  2. El valor 4 se mete en la variable número y se “machaca” o sobreescribe su viejo valor con lo que ahora numero almacena el valor 4.

Literales

los valores constantes que aparecen en el código se llaman literales o equivalentemente constantes literales. En el siguiente ejemplo

var nombre = "Juan Perro"
var edad = 15

“Juan Perro” y 15 son literales.

Expresiones

De forma informal, una expresión es una especie de frase o fórmula formada por una combinación de variables, literales, operadores y otros recursos que finalmente se pueden evaluar, es decir, calcular para reducir la expresión a un único valor. Por ejemplo

  • 3+2 es una expresión que al evaluarse se reduce al valor 5
  • “Hola “+ “Winchi” se reduce a “Hola Winchi”
  • Si a es una variable que almacena el valor 10, la expresión a+3 se evalua y se obtiene el valor 13
  • etc.

Todos los lenguajes tienen normas muy parecidas para la construcción de expresiones pero cada uno tiene sus pequeños matices. Por ejemplo: “hola”*2 es una expresión errónea (imposible) en Kotlin, peo es correcta en python que la reduce al valor “holahola”

Uso de variables con la función print()

Ya la utilizamos en ejemplos previos. Simplemente al indicar el nombre de la variable se lee su valor y se imprime

fun main() {
    var nombre = "Juan Perro"
    var edad = 15
    println(nombre)
    println(edad)
}

Uso de + y $ con la función print()

El operador + se puede usar para concatenar cadenas de caracteres. Estudiaremos la concatenación de cadenas de caracteres más adelante. Por el momento observamos su uso con print()

fun main() {
    var nombre = "Juan Perro"
    var edad = 15
    println(nombre)
    println("Tu nombre: "+ nombre)
    println("Tu edad: "+ edad)
}

Observa que para concatenar cadenas de texto literales con variables dentro de un print() necesitamos indicar dicha concatenación con el operador +. Puedes comprobar el error de compilación sin en el ejemplo anterior suprimimos el + en el primer println()

println("Tu nombre: " nombre)

Una forma alternativa de mezclar valores de variables con cadenas de texto es usando el operador $. En este caso escribimos la variable dentro de las comillas de la cadena, no usamos el + y anteponemos el $ al nombre de la variable

fun main() {
    var nombre = "Juan Perro"
    var edad = 15
    println(nombre)
    println("Tu nombre: $nombre")
    println("Tu edad:  $edad")
}

Usar var para crear variables mutables

Mutable significa que a la variable se puede reasignar a un valor diferente después de la asignación inicial. Normalmente esto es lo que deseamos hacer con una variable, ir variando su valor a lo largo del programa.

Para declarar una variable mutable, usamos la palabra clave var como ya hicimos en los ejemplos anteriores. En el siguiente ejemplo observamos que efectivamente una variable declarada con var puede cambiar de valor

fun main() {
    var nombre = "Juan Perro"
    println("Tu nombre: $nombre")
    nombre="otro nombre"
    println("Nuevo nombre: $nombre")
}

Usar val para crear variables inmutables

usando val, en lugar de var, una vez que se asigna un valor a la variable, no se le puede cambiar de valor en el resto del programa. Este código genera error

fun main() {
    val nombre = "Juan Perro"
    println("Tu nombre: $nombre")
    nombre="otro nombre"
    println("Nuevo nombre: $nombre")
}

A las variables inmutables también se les llama variables de sólo lectura, variables constantes o simplemente constantes Cuando sabemos que vamos a usar un valor que no tiene ni debe cambiar a lo largo del programa es una buena práctica de programación asignarlo a una variable definida con val

Reglas de nomenclatura de variables de Kotlin

Hay ciertas reglas que se deben seguir al nombrar las variables de Kotlin:

  • Los nombres de variables de Kotlin pueden contener letras, dígitos, guiones bajos y signos de dólar.

  • Los nombres de las variables de Kotlin deben comenzar con una letra, $ o guiones bajos

  • Las variables de Kotlin distinguen entre mayúsculas y minúsculas, lo que significa que Zara y ZARA son dos variables diferentes.

  • La variable Kotlin no puede tener ningún espacio en blanco u otros caracteres de control.

  • La variable de Kotlin no puede tener nombres como var, val, String, Int porque son palabras clave reservadas en Kotlin.

Última actualización: 23.09.2025

Palabras reservadas

Las palabras reservadas, también llamadas clave o predefinidas, son palabras que tienen significados especiales para el compilador y no se pueden usar. Ya usamos palabras reservadas, por ejemplo:

  • fun para indicar que vamos a definir una función
  • var o val para definir una variable En kotlin se distingue entre palabras reservadas duras y suaves:
  • duras son aquellas palabras que jamás se pueden usar como identficadores
  • blandas son aquellas palabras que en ciertos contextos son palabras reservadas pero en otros contextos se pueden usar libremente como identificadores.

Las palabras reservadas se van aprendiendo poco a poco pero veamos una lista de algunas palabras reservadas duras:

  • var
  • val
  • fun
  • if
  • else
  • when
  • do
  • for
  • while
  • return
  • class
  • true
  • false
  • etc.

Observa como en el siguiente ejemplo se generan errores de compilación al intentar crear una variable que se llame fun. Recuerda que fun es una palabra reservada y no se puede usar como nombre de variable.

fun main() {
    val  fun = "Juan Perro"
}
Última actualización: 23.09.2025

tipos de datos y operadores

Tipos de datos y operadores más básicos

Última actualización: 23.09.2025

Subsecciones de tipos de datos y operadores

Tipos de datos básicos

Kotlin es un lenguaje de tipado estático, lo que significa que el tipo de datos de cada expresión debe conocerse en el momento de la compilación.

Los tipos básicos

En Kotlin todo es un objeto(entenderemos esto más adelante) y todo objeto tiene un tipo. Los siguientes por su importancia y uso frecuente les llamamos tipos básicos y son:

  • númericos
    • Numeros enteros
      • Byte
      • Short
      • Int
      • Long
    • Números reales
      • Float
      • Double
  • Boolean
  • Char
  • String
  • Array

Indicar el tipo de una variable

Se puede indicar el tipo de una variable de dos formas:

  • explicitamente
  • usando el mecanismo de inferencia automática de tipos

Explicitamente

Indicándolo despues del nombre de la variable y “:”

var unaVariable: Int = 3
val otraVariable: String ="hola"

Con inferencia de tipos

Si no se indica el tipo, Kotlin lo infiere del valor de la expresión a la derecha de la expresión de asignación

var unaVariable  = 3 //implica tipo Iint
val otraVariable ="hola" //implica tipo String

Cambio de tipo de una variable

Esta es una cuestión con muchos matices que se abordarán más adelante, pero como punto de partida, una vez que una variable se crea, es de un tipo y mantiene su tipo hasta el final de su vida y no se le pueden asignar valores de diferentes tipos.

var unEntero  = 3
unEntero=7 //OK
unEntero="hola"  //ERROR
Última actualización: 23.09.2025

Tipos Numéricos

Tipos para números enteros

Para números enteros, hay cuatro tipos con diferentes tamaños y, por lo tanto, rangos de valores.

Tipo Tamaño (Bits) Valor mínimo Valor máximo
Byte 8 -128 127
Short 16 -32768 32767
Int 32 -2,147,483,648 (-2 31 ) 2.147.483.647 (231 - 1)
Long 64 -9,223,372,036,854,775,808 (-2 63 ) 9.223.372.036.854.775.807 (2 63 - 1)

Literales para números enteros

Son los literales que especifican un valor numérico. Pueden ser valores enteros o reales.

Los literales enteros se pueden escribir en base 10, base 2 o base 16. No se admite base 8. Para escribir en hexadecimal añadimos como prefijo al literal 0x y en base 2 añadimos 0b

fun main() {
    var valor15EnBase10  = 15
    var valor15EnBase16= 0xF
    var valor15EnBase2= 0b1111
    println(valor15enBase10)
    println(valor15enBase10)
    println(valor15EnBase2)
}

Por otro lado los literales pueden ser Int y Long. Para almacenarlos con 64 bits específicamente hay que añadir una L al final del número, por ejemplo si usamos 1L, el valor 1 en este caso se almacena en 64 bits

Inferencia de tipos enteros

Todas las variables inicializadas con valores enteros que no excedan el valor máximo de Int tienen el tipo inferido Int. Si el valor inicial excede este valor, entonces el tipo es Long. Para especificar que el valor sea explícitamente Long se agrega el sufijo L al valor.

val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1 //Byte

Tipos para números reales

Tipo Tamaño (Bits) Valor mínimo Valor máximo
Float 16 1.40129846432481707e-45 3.40282346638528860e+38
Double 32 4.94065645841246544e-324 1.79769313486231570e+308

Literales reales

Para especificar explícitamente el tipo Float de un valor se usa el sufijo f o F. Si dicho valor contiene más de 6-7 dígitos decimales, se redondeará.

val pi = 3.14 // Double
// val one: Double = 1 // Error: type mismatch
val oneDouble = 1.0 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817
Última actualización: 23.09.2025

Tipos básicos No Numéricos

Breve introducción. Se veran con más detalle más adelante

Boolean

El tipo Boolean representa objetos booleanos que pueden tener dos valores: true y false.

fun main() {
   val a: Boolean = true   
   val b: Boolean = false  

   println("Value of variable a "+ a )
   println("Value of variable b "+ b )
}

Char

Se usa para almacenar un solo carácter. Un valor Char debe estar entre comillas simples, como ‘A’ o ‘1’.

fun main() {
   val letter: Char    
   letter = 'A'        
   println("$letter")
}

Secuencias de escape para representar caracteres

Una secuencia de escape esta formada por una barra inversa seguida de una letra, un carácter o de una combinación de dígitos. Una secuencia de escape siempre representa un solo carácter aunque se escriba con dos o más caracteres. Por ejemplo:

  • \t para tabulador
  • \n para salto de línea
  • \"" para comillas dobles
  • otros Las secuencias de escape se usan en varias situaciones. Iran salidendo casos a lo largo del curso en el que necesitaremos el uso de secuencias de escape y los iremos explicando a medida que surjan. Veamos un ejemplo sencillo, queremos insertar dentro de un String un salto de línea
fun main() {
    println("A continuación viene  un salto de línea\n y ya estoy en otra línea")
}

Otros

Otros tipos muy importantes, que se utilizan mucho ya desde los primeros pasos en programación como String y Array se explicarán en lecciones a parte.

Última actualización: 23.09.2025

Conversión de Tipos

La conversión de tipos es un proceso en el que el valor de un tipo de datos se convierte en otro tipo. Kotlin no admite la conversión directa entre tipos . Por ejemplo, no es posible convertir un tipo Int a un tipo Long. El siguiente código genera error de compilación

fun main(args: Array<String>) {
   val x: Int = 100
   val y: Long = x  // ERROR
   println(y)
}

Para convertir un tipo de datos a otro tipo, Kotlin proporciona un conjunto de funciones:

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()
  • toChar()
  • toString()

ReescribImos el ejemplo anterior para que sea OK

fun main(args: Array<String>) {
   val x: Int = 100
   val y: Long = x.toLong()
   println(y)
}

Observa que las funciones de conversión se escriben nombreVar.funConversion() Realmente es una forma especial de función llamada método, concepto que estudiaremos y entenderemos más adelante.

Conversiones y el operador +

el operador + con números hace sumas aritméticas pero con strings concatena las cadenas. ¿Que ocurre si + tiene un operador de tipo String y otro de tipo numérico?

En el siguiente ejemplo se observa que el + convierte automáticamente el número a String y finalmente concatena ambos operadores

fun main() {
    val saludo="hola"
    val numero=7
    println(saludo + numero)
}

En cambio, si el primer operando es el de tipo numérico da error de compilación

fun main() {
    val saludo="hola"
    val numero=7
    println(numero + saludo)
}

la solución para este tipo de situación es usar toString() de forma que el dato numérico lo convierte a String y el + simplemente concatena Strings

fun main() {
    val saludo="hola"
    val numero=7
    println(numero.toString() + saludo)
}
Última actualización: 23.09.2025

Operadores

Un operador es un símbolo que le dice al compilador que realice manipulaciones matemáticas o lógicas específicas. Kotlin es rico en operadores integrados y proporciona los siguientes tipos de operadores:

  • Operadores aritméticos
  • Operadores relacionales
  • Operadores de Asignación
  • Operadores unarios
  • Operadores logicos
  • Operacdores de nivel de bit

Operadores aritméticos

Se utilizan para realizar operaciones matemáticas básicas: suma, resta, multiplicación, y división.

Operador Nombre descripción Ejemplo
+ Suma Suma dos valores x+y
+ Suma Suma dos valores x+y
- Resta Resta un valor de otro x-y
* Multiplicación Multioplica dos valores x*y
/ División Obtiene el cociente de dividir un valor por otro x/y
% Módulo Obtiene el resto de dividir un valor por otro x%y
fun main() {
   val x: Int = 40
   val y: Int = 20

   println("x + y = " +  (x + y))
   println("x - y = " +  (x - y))
   println("x / y = " +  (x / y))
   println("x * y = " +  (x * y))
   println("x % y = " +  (x % y))
}

Operadores relacionales

Los operadores relacionales (de comparación) se utilizan para comparar dos valores y devuelven un valor booleano : true o false .

Operador Nombre Ejemplo
> mayor que x>y
< mayor que x>y
>= mayor o igual que x>=y
<= menor o igual que x<=y
== igual a x==y
!= distinto que x!=y
fun main() {
   val x: Int = 40
   val y: Int = 20

   println("x > y = " +  (x > y))
   println("x < y = " +  (x < y))
   println("x >= y = " +  (x >= y))
   println("x <= y = " +  (x <= y))
   println("x == y = " +  (x == y))
   println("x != y = " +  (x != y))
}

Operadores de asignación de Kotlin

Como ya sabemos, el operadores de asignación = se utilizan para asignar valores a las variables. Este es el operador de asignación más importante y habitual pero hay otros complementarios.

Operador Ejemplo Forma expandida
= x=10 x=10
+= x+=10 x=x+10
-= x-=10 x=x-10
*= x*=10 x=x*10
/= x/=10 x=x/10
%= x%=10 x=x%10
fun main() {
   var x: Int = 40

   x += 5
   println("x += 5 = " + x )
   
   x = 40;
   x -= 5
   println("x -= 5 = " +  x)
   
   x = 40
   x *= 5
   println("x *= 5 = " +  x)
   
   x = 40
   x /= 5
   println("x /= 5 = " +  x)
   
   x = 43
   x %= 5
   println("x %= 5 = " + x)
}

Operadores unarios

Los operadores unarios requieren solo un operando; realizan varias operaciones, como incrementar/disminuir un valor en uno, negar una expresión o invertir el valor de un booleano.

Operador Nombre Ejemplo
+ más unario +x
- menos unario -x
++ incrementar en 1 x o x
disminuir en 1 –x o x–
! invierte el valor de un booleano !x
fun main() {
   var x: Int = 40
   var b:Boolean = true

   println("+x = " +  (+x))
   println("-x = " +  (-x))
   println("++x = " +  (++x))
   println("--x = " +  (--x))
   println("!b = " +  (!b))
}

Los operadores ++ y – tienen forma prefija y forma postfija. Estas formas no son siempre equivalentes , esto se estudiará más adelante, por el momento si se utiliza sobre variables aisladas, no insertas en una expresión más complejas, son equivalentes.

Operadores lógicos

Los operadores lógicos son: && || !

Trabajan con valores booleanos.Los vemos en el documento expresiones booleanas

Precedencia de operadores y uso de paréntesis

Observa la expresión 2+3*4. Siguiendo las reglas de las matemáticas tradicionales sabemos que el * tiene más precedencia que el + y por tanto la expresión anterior es equivalente a 2+(3*4). En los lenguajes de programación el concepto de precedencia de operadores es similar a la precedencia con los operadores de las matemáticas tradicionales, así que, razonando matemáticamente cubrimos la mayor parte de los casos de precedencia en expresiones de programación. Hay no obstantes ciertos matices y diferencias entre ambos . Si dudas, simplemente, ¡usa paréntesis!. Entiende a la perfección la salida del siguiente ejemplo:

fun main() {

    val a=2
    val b=3
    //quiero sumar a y b y el resultado multiplicarlo por 4
    val resultado1=(a+b)*4

    //las  dos siguientes expresiones son equivalentes.
    val resultado2=a+b*4
    val resultado3=a+(b*4)

    println(resultado1)
    println(resultado2)
    println(resultado3)
}

Mezcla de tipos en expresiones.

Es una cuestión con muchos matices. Por el momento, para simplificar, utiliza operandos del mismo tipo.

si mezclamos tipos pueden ocurrir una diversidad de situaciones, por ejemplo, el siguiente código da como salida false

fun main() {
    var x:Int=1
    var y:Double=1.0
    print(x>y)
}

y en cambio este da error de compilación

fun main() {
    var x:Int=1
    var y:Double=1.0
    print(x==y)
}

Por el momento, para simplificar, utiliza operandos del mismo tipo. Recuerda que cuando necesites cambiar un valor de un tipo a otro dispones de funciones de conversión de tipo.

Última actualización: 23.09.2025

Expresiones booleanas

Muchas veces nos encontramos con una situación en la que necesitamos tomar una decisión del tipo Sí o No , o queremos indicar que algo es Verdadero o Falso (true o false). Para manejar tal situación usamos el tipo de datos booleano.

Literales booleanos

Son las palabras reservadas true y false.

Variables booleanas

Una variable booleana se crea con el tipo Boolean y puede almacenar los valores true o false

fun main(args: Array<String>) {
   val isSummer: Boolean = true
   val isCold: Boolean = false
  
   println(isSummer)
   println(isCold)
   
}

Operadores booleanos o lógicos

Operador Nombre Descripción Ejemplo
&& Y lógico (and lógico) Devuelve verdadero si ambos operandos son verdaderos x&&y
|| O lógico (or lógico) Devuelve verdadero si alguno de los operandos es verdadero x||y
! No lógico (not) Es unario. Devuelve falso si el operando es verdadero y viceversa !x
fun main() {
   var x: Boolean = true
   var y:Boolean = false

   println("x && y = " +  (x && y))
   println("x || y = " +  (x || y))
   println("!y = " +  (!y))
}

Expresiones booleanas con operadores relacionales

Una expresión booleana tras ser calculada da true o false. Las expresiones booleanas pueden formarse con operadores booleanos como vimos en el ejemplo anterior, pero también con expresiones con operadores relacionales

fun main() {
    val x: Int = 40
    val y: Int = 20
    val z:Boolean=false
    println(x > y || x==y)
    println(x > y && x==y)
    println(z || x==y)
}
Última actualización: 23.09.2025

colecciones básicas

Las colecciones son utilizadas para agrupar y manipular conjuntos de datos de manera eficiente. Proporcionan métodos y operaciones para agregar, eliminar, buscar y acceder a los elementos almacenados en ellas.

Las colecciones más básicas del lenguaje kotlin son: strings, arrays, listas, rangos y mapas.

Por el momento estudiaremos sólo Strings y haremos una breve reseña a las listas y rangos ya que con estos recursos nos basta para enfrentarnos a gran cantidad de interesantes retos algorítimicos sin necesidad de manejar ingentes cantidades de sintaxis. Recuerda que lo más importante en un estudiante de programación es desarrollar la capacidad de enfrentarse a problemas. El conocimiento profundo de un lenguaje es importante pero un escalón inferior a la capacidad mencionada.

Última actualización: 23.09.2025

Subsecciones de colecciones básicas

String

Los objetos Strings se utilizan constatemente en programación y son importantes e inevitables incluso para hacer programas básicos. Por esta razón vemos ahora recursos esenciales para trabajar con Strings para poder incorporar estos recursos en nuestros mini programas. Por otro lado, a lo largo del curso seguiremos ampliando información sobre este importantísimo objeto.

Qué es un String

Es un objeto que contiene una cadena de caracteres.

Literales String

Hay dos tipos de literal String:

  • El String escapado: se declara entre comillas dobles (" “) y puede contener caracteres de escape como ‘\n’, ‘\t’, ‘\b’, etc.

  • El String sin formato(raw String): se declara entre comillas triples (”"" “”") y puede contener varias líneas de texto sin ningún carácter de escape.

fun main() {
   val escapedString : String  = "I am escaped String!\n"
   var rawString :String  = """This is going to be a
   multi-line string and will
   not have any escape sequence""";

   print(escapedString)
   println(rawString)
}

Concatenación en Strings con el operador +

Ya indicamos con anterioridad que el efecto del operador + con Strings es la concatenación. Se pueden concatenar literales, variables o una mezcla

fun main() {
    var palabra1 : String = "Hola"
    var palabra2 : String = "Mundo"
    var miString: String
    miString=palabra1+palabra2
    println(miString)
    println("Adios" + " mundo cruel")
}

String Templates (plantilla en String)

Son fragmentos de código que se evalúan y cuyos resultados se insertan en la cadena. Una plantilla comienza con un signo de dólar $ y puede constar simplemente de un nombre de variable o de una expresión más compleja entre {}.

fun main() {
    var unString : String = "Hola 2 +2 es ${2+2}"
    println(unString)
    var otroString: String = "Insertamos el contenido de unString $unString"
    println(otroString)
}

Indices en String

Un String es una secuencia de caracteres y se puede acceder individualmente a cada uno de ellos especificando un índice asociado a la posición del caracter .

Al primer caracter le corresponde el indice 0, al segundo el 1, … El índice se indica entre corchetes []

fun main() {
    var saludo : String = "Hola mundo"
    println(saludo[0])
    println(saludo[2])
    println(saludo[4])//el quinto caracter es un espacio
    println(saludo[7])
}

El String es un objeto

En Kotlin todo es un objeto. Un String también es un objeto. Aun no estudiamos que es un objeto pero para trabajar con Strings no es necesario avanzar que un objeto consta de propiedades y funciones. Para acceder a ambos utilizamos el operador “.”.

Propiedades del objeto String

Cuando estudiemos programación orientada a objetos entenderemos al cien por cien que es una propiedad. Por el momento piensa que una propiedad es uuna suerte de variable interna del objeto a la que se puede acceder a través del operado punto El objeto String tiene dos propiedades usadas frecuentemente:

  • length
  • lastIndex. Su valor es equivalente a length -1 ya que recuerda que los índices comienzan a numerarse por 0
fun main() {
   var saludo : String = "Hola mundo"
   println(saludo.length)
   println(saludo.lastIndex)
}

A las funciones también se accede con el operador punto. El resto de este capítulo muestra ejemplos de algunas de las funciones más importantes del objeto String

Los String son inmutables.

Los strings Kotlin son inmutables. Esto significa que una vez que se crea un string, no se puede modificar. Cualquier operación que cambie el contenido de un string crea un nuevo string.

Por ejemplo, el siguiente código crea un nuevo string cada vez que se llama a la función toUpperCase():

fun main() {
  val cadena = "Hola, mundo!"

  println(cadena.uppercase()) // Imprime "HOLA, MUNDO!"
  println(cadena) // Imprime "Hola, mundo!" y demuestra que el String inicial no se modificó
}

Si queremos trabajar con el nuevo String debemos engancharlo a una variable

fun main() {
    val cadena = "Hola, mundo!"
    val nuevoString=cadena.uppercase()
    println(nuevoString)
}

Si queremos conseguir el efecto anterior pero sin crear una variable adicional, podemos asignar el nuevo string a la variable inicial. Observa que ahora como la variable puede cambiar de String tiene que definirse como var.

fun main() {
    var cadena = "Hola, mundo!"
    cadena=cadena.uppercase()
    println(cadena)
}

ya que los String son inmutables, operaciones de asignación con el operador [] no están permitidas.

el siguiente código genera error de compilación

fun main() {
    var cadena = "Hola, mundo!"
       cadena[0]='X'//cambiar H por X así no es posible
}

Funciones del objeto String

Cuando estudiemos programación orientada a objetos entenderemos al cien por cien que es una función miembro asociada a un objeto y por tanto una función de un String. Por el momento es suficiente pensar que las funciones de un String son funciones cuyos datos de trabajo son los datos del String. Se accede a ellas a través del operador punto. Veremos algunas de las funciones más relevantes de los objetos String.

equals() para comparar dos cadenas

Se puede utilizar para comprobar igualdad el operador == o el método equals().

fun main() {
    var str1 = "hola"
    var str2 = "mundo"
    var str3 = str1+str2
    var str4 = str1+str2
    println(str3.equals("holamundo"))
    println(str3=="holamundo")
    println(str3==str4)
}

uppercase() y lowercase()

uppercase() y lowercase() para convertir una cadena en mayúsculas y minúsculas, respectivamente.

fun main() {
    var saludo : String = "Hola Mundo"
    println(saludo.lowercase())
    println(saludo.uppercase())
}

drop() y dropLast() eliminar los primeros o los últimos caracteres de una cadena

fun main() {
    var saludo : String = "Hola Mundo"
    println(saludo.drop(2))
    println(saludo.dropLast(2))
}

indexOf() para encontrar posición de una subcadena

fun main() {
   var frase : String = "Siempre me dices que la vida que llevo es horrible"
   println(frase.indexOf("que")) //17 es la posición del primer que
   println(frase.indexOf("la"))
   println(frase.indexOf("jamones"))
}

Ejecuta el código y comprueba los índices que devuelve. Observa que cuando no existe el substring que se le indica devuelve -1

subSequence() para indicar un subtring por índice

fun main() {
    val str1 = "abcdefghij"
    val startIndex = 2
    val endIndex = 7
    val substring = str1.subSequence(startIndex, endIndex)
    println("El substring es : " + substring)
}

Ejecutando el ejemplo anterior observa que:

  • el límite inferior indicado es inclusivo
  • PERO, el límite superior es exclusivo

La función replace()

Tiene varias sintáxis pero lo más básico es indicar el String que quiero cambiar por el nuevo. A menudo el String que se quiere cambiar consiste como en el ejemplo en único caracter pero no necesariamente.

fun main() {
    var str = "la vida es  dura"
    val oldValue = "a"
    val newValue = "i"
    val output = str.replace(oldValue, newValue)
    print(output)
}

Un uso muy habitual de replace() es eliminar espacios en blanco

fun main() {
    var str = "      la vida         es  dura             "
    val oldValue = " "//espacio en blanco
    val newValue = ""//String vacío
    val output = str.replace(oldValue, newValue)
    print(output)
}

Funciones de conversión de String a otro tipo de dato

Hay una serie de funciones para convertir un String en otro tipo de dato. Las de uso más inmediato necesarias ya en los primeros pasos de programación, son las funciones que convierten un String en un formato numérico como toInt(), toDouble() etc.

Asegurate de entender a la perfección la salida del siguiente código

fun main() {
    val str1 = "1"
    val str2 = "2"

    val num1=1
    val num2=2

    println(str1+str2)
    println(num1+num2)
    println(str1.toInt()+str2.toInt())
    
}

Observa que el String que se quiere pasar a formato numérico tiene que contener sólo los caracteres del número, incluso los espacios en blanco al final generan error.

fun main() {
    val x = "1"
    val y = "1 "//hay un espacio al final

    println("x convertido vale "+ x.toInt())
    println("a ver que paso cuando lo intento con y")
    println("y convertido vale "+ y.toInt())
}
Última actualización: 23.09.2025

Listas

Una lista es un conjunto de valores del mismo tipo a los que podemos acceder a través de una sola variable. Por lo tanto la operación más básica con una lista es crearla y asignarla a una variable

    val miLista = listOf(1, 2, 3)

¿Como se accede a cada elemento individual de la lista?. En una lista cada elemento tiene una posición y podemos usar dicha posición para acceder a cada elemento individual. El concepto y sintaxis es similar al acceso por índices estudiado con Strings.

fun main() {
    //val miLista: List<Int> = listOf(1, 2, 3)//el tipo en este caso lo puede inferir el compilador
    val miLista = listOf(1, 2, 3)
    println("imprimir toda la lista junta $miLista") // [1, 2, 3]
    println("imprimir la lista elemento a elemento ")
    println(miLista[0])
    println(miLista[1])
    println(miLista[2])
    println("el tamaño de la lista es:  "+ miLista.size)
}

Hay dos tipos básicos de listas:

  • inmutables
  • mutables. Permiten modificar el valor de sus elementos así como añadir/borrar elementos a la lista, es decir, modificar el tamaño de la lista.

El ejemplo anterior de listas se corresponde con una lista inmutable, para lo cual utilizamos la función listOf(). A continuación veremos un ejemplo de lista inmutable.

Ejemplo de lista mutable

Hay varias formas de crear una lista mutable, Vemos un ejemplo con mutableListOf()

fun main() {
    val colorsList = mutableListOf("Amarillo", "Azul", "Rojo")

    colorsList.add("Verde") // [Amarillo, Azul, Rojo, Verde] //inserta al final
    colorsList.add(0, "Blanco") // [Blanco, Amarillo, Azul, Rojo, Verde]//inserta en la posición indicada indicada
    colorsList.removeAt(2) // [Blanco, Amarillo, Rojo, Verde]
    //observa como modificamos con []
    colorsList[1] = "Negro" // [Blanco, Negro, Rojo, Verde]
    println(colorsList)
    println(colorsList[0])
}

declarar una lista mutable de tamaño 0 (vacía)

Podemos querer ir construyendo una lista partiendo de una lista vacia. Al partir de una lista vacía Kotlin no puede inferir el tipo de la lista. La solución es incluir el tipo en la declaración de la lista de alguna manera como en el ejemplo.

fun main(){
    var lista= mutableListOf<Int>()//lista de Int de tamaño 0
    println(" tamaño lista ${lista.size}")
    lista.add(99)
    println(" tamaño lista ${lista.size}")
}

La funcion split() de los Strings

split() permite trocear o dividir un String en trocitos más pequeños y estos trozos los devuelve en un lista. Como parámetro se le indica el criterio de división o delimitador. Por ejemplo el delimitador en el siguiente ejemplo es el String “:”

fun main() {
    val str = "A:B:C:que bonito:z zz"
    val delim = ":"

    val list = str.split(delim)

    println(list)    // [A, B, C, que bonito, z zz]
}

El delimitador realmente es una expresión regular pero de momento con pensar que es un delimitador es un caracter que se utiliza como punto de corte es suficiente.

Uno de los usos más frecuentes es querer dividir un texto en palabras utilizando como delimitador el espacio en blanco.

fun main() {
    val str = "Había una vez un circo que alegraba siempre la ilusión"
    val delim = " "

    val list = str.split(delim)

    println(list)    // [Había, una, vez, un, circo, que, alegraba, siempre, la, ilusión]
}

Utilizaremos split() para com combinar con el readln() para conseguir un estilo de entrada de datos por teclado que veremos más adelante.

Última actualización: 23.09.2025

Rangos

Un rango en Kotlin es tipo que engloba un conjunto de valores que representa el concepto matemático de intervalo de valores. Es decir, es un subconjunto de elementos comprendidos entre un extremo inferior a y un extremo superior b.

Por ejemplo, el rango [0,5] representa los valores enteros del 0 al 5. Para crearlo se usa la función operador toRange()

fun main() {
    val fromZeroToFive = 0.rangeTo(5)
    println(fromZeroToFive) // 0..5
}

Otra sintaxis alternativa y más usada en la práctica es el formato a..b .

fun main() {
    val fromZeroToFive = 0..5
    println(fromZeroToFive) // 0..5
}

La aplicación práctica de los rangos la veremos al estudiar estructuras de control(if,for etc.). Ahora nos centramos brevemente en su concepto.

Rangos con extremos inclusivos y exclusivos

Ya que un rango representa un intervalo matemático se me viene a la cabeza lo de intervalo abierto, cerrado, abierto por la derecha pero cerrado por la izquierda etc. que finalmente consisten en pensar si al especificar los extremos están incluidos o no. Piensa que esto es un problema cotidiano, si te digo, tienes hasta el viernes para entregar el trabajo, tu me preguntarás… ¿con el viernes incluido?

Rangos con extremos inclusivos con ..

Sintácticamente se consiguen como vimos más arriba, con los ..

fun main() {
    val rangoInclusivo = 1 .. 5 // El rango incluye 1, 2, 3, 4 y 5.
    println(rangoInclusivo) // 0..5
}

Rango con extremo superior no inclusivo con until

fun main() {
    val rangoInclusivo = 1 until 5 // El rango incluye 1, 2, 3, 4 
    println(rangoInclusivo) // 0..4
}

Rango con extremo inferior no inclusivo.

No hay una sintaxis propia. Si el intervalo es de números enteros simplemente se incrementa en 1 el extremo inferior.

val rangoExclusivoPorIzquierda = (inicio + 1)..fin

Especialmente interesante puede ser “imitar” un intervalo abierto por la izquierda, es decir, con el extremo inferior no incluido cuando el intervalo es de double. Para esto, puedes hacerlo ajustando el límite inferior sumando un valor muy pequeño.

fun main() {
    val limiteInferior = 1.0
    val limiteSuperior = 5.0
    val epsilon = 1e-10 // Valor muy pequeño

    val rangoExclusivoPorIzquierda = (limiteInferior + epsilon)..limiteSuperior
    println(rangoExclusivoPorIzquierda)

}

Rangos y el operador in

Puedes usar el operador in para verificar si un valor está contenido dentro de un rango. El operador in devuelve true si el valor está dentro del rango y false en caso contrario.

fun main() {
    val limiteInferior = 1.0
    val limiteSuperior = 5.0
    val epsilon = 1e-10 // Valor muy pequeño

    val rangoExclusivoPorIzquierda = (limiteInferior + epsilon)..limiteSuperior
    println(3.2 in rangoExclusivoPorIzquierda)
    println(1.0  in rangoExclusivoPorIzquierda)
}

Más sobre rangos.

Hay muchas más detalles adicionales relacionadas con rangos, pero se examinan mejor cuando se usan con estructuras de control. En ese momento ampliaremos más sobre sintáxis y conceptos sobre rangos.

Última actualización: 23.09.2025

Entrada de datos por teclado con readln()

La forma más basica de introducir datos con teclado es utilizando la función readln().
La función readln() lee una línea de la entrada standard y la devuelve al programa sin incluir el enter que marca el fin de línea.

¿Qué es la entrada standard ?

Cuando a una función como readln() no se le especifica el dispositivo del que tiene que leer la información de entrada, utiliza el dispositivo asociado la entrada standard que por defecto es el teclado. Si no especificamos lo contrario la entrada standar es el teclado.

¿Qué es una línea?

Es similar a una línea de un folio, es decir un conjunto de caracteres. A este conjunto de caracteres se le suele llamar por su nombre en ingles String. La diferencia entre una línea de un folio y una línea en datos informáticos es como se marca el fin de la linea. En un folio el fin de la línea viene determinado por el final físico del folio hacia la derecha, en informática el fin se establece grabando un caracter de fin de línea también llamado caracter de salto de línea.

El carácter de salto de línea

Es un caracter del código ascii igual que la letra ‘A’ o el caracter ‘$’. Concretamente dentro del código ASCII tiene el valor decimal 10 o hexadecimal 0A, pero desde el código fuente se suele suele representar más cómodamente como ‘\n’. Desde el teclado físico, este caracter se envia a través de la tecla conocida por enter, intro return o salto de línea.

El proceso de capturar datos por teclado con readln

  1. El usuario teclea un string de caracteres.
  2. Pulsa enter.
  3. El sistema operativo pone a disposición de la función readln el string terminado con el enter
  4. La función readln devuelve al programa el string sin el enter
fun main() {
    println("Teclea tu nombre")
    val nombre = readln()
    print("¡Hola, $nombre!")
    print(" bonito nombre")
}

Observa como efectivamente el readln() no devuelve el enter ya que bonito nombre se escribe a continuación del saludo, sin salto de línea. Un ejemplo de ejecución podría ser

Teclea tu nombre
koki kiko
¡Hola, koki kiko! bonito nombre

Cómo leer un valor numérico por teclado

Es muy importante tener en cuenta que la función readln siempre devuelve un String. Así que en realidad lo que vamos a discutir a continuación tiene que ver con el procesamiento de un string, no con la entrada de teclado.

Es habitual que un string contenga caracteres numéricos y que se quiera operar numéricamente con ellos. Para poder operar númericamente con un String que contiene un número debemos convertir dicho String explícitamente a formato númerico con las funciones ya vistas en conversión de tipos.

En el siguiente ejemplo la variable numero es realmente un String y la ejecución del programa genera error ya que no se permite hacer multiplicaciones aritméticas con un valor String

fun main() {
    println("Teclea un número")
    val numero  = readln()
    val doble =numero*2
    println("El doble es $doble")
}

Por lo tanto, debemos convertir a numérico el string que contiene la variable numero. Podemos por ejemplo convertirlo a un número entero con toInt().

fun main() {
    println("Teclea un número")
    val numero  = readln()
    val doble =numero.toInt()*2
    println("El doble es $doble")
}

Cómo leer varios valores de la misma línea separados entre sí por un espacio en blanco

En general, lo importante es tener encuenta que readln() simplemente devuelve un String, a continuación las instrucciones de mi programa deberán procesar dicho String a nuestro gusto para obtener el efecto deseado.

Por ejemplo, supongamos que la entrada por teclado consiste en introducir tres números enteros en una línea. Los números los vamos a escribir separados entre sí por un espacio en blanco. Queremos averiguar la media aritmética de los tres números, por lo tanto necesito sumar y dividir viéndome entonces forzado a obtener de la línea de entrada tres valores numéricos. Una forma facil es utilizando la función split() de los objetos String. La función split() trocea utilizando como separador el caracter indicado y devolviendo los trozos en una lista. En realidad, el parámetro de split es una expresión regular pero por el momento nos basta con pensar que es un caracter en base al cual hacer el troceo. En nuestro ejemplo un espacio en blanco es el caracter de “troceo”.

fun main() {
   val linea= readln()
   val lista= linea.split(" ")
   val a= lista[0].toInt()
   val b= lista[1].toInt()
   val c= lista[2].toInt()
   val suma= a+b+c
   val media= suma/3.0
   println(media)
}

una posible ejecución

2 5 1
2.6666666666666665
Última actualización: 23.09.2025

control de flujo

El orden de ejecución de las sentencias de un programa es en principio secuencial, es decir, de arriba hacia abajo, comenzando por la primera hasta llegar a la última. Al orden de ejecución se le llama flujo. El flujo secuencial elemental de un programa se puede alterar con las instrucciones de control de flujo que veremos a continuación

Última actualización: 23.09.2025

Subsecciones de control de flujo

Control de flujo

Un programa es un conjunto de sentencias. El orden básico de ejecución de las sentencias es secuencial de arriba abajo. No obstante, hay sentencias especiales que pueden modificar este orden secuencial. Al orden de ejecución le llamamos flujo de ejecución y a las sentencias que permiten modificar el flujo secuencial de ejecución se le llaman sentencias de control de flujo.

Las sentecias de control de flujo kotlin son:

  • Condicionales
    • if
    • when
  • bucles
    • for
    • while
    • break/continue
Última actualización: 23.09.2025

Sentencia condicional IF

if simple

Permite ejecutar o no un bloque de instrucciones en función de una condición . Sintaxis:

if (condition) {
   // bloque de código ejecutado si la condición es cierta
}

La condición será una expresión booleana que al evaluarse valdrá por tanto true/false

fun main() {
    val age:Int = 10
    if (age > 18) {
        print("Adult")
    } 
}

En este caso no se imprimirá nada. Si inicializamos la variable con 19 o un valor mayor sí se imprimiría “Adult”.

Si el bloque de instrucciones consiste en una única instrucción como en el ejemplo de arriba se pueden omitir la llaves de bloque {}

if .. else

Es una extensión del if simple de forma que ahora hay dos bloques de código un bloque A asociado al if y otro B asociado al else. Si la condición es false, se ejecuta el código del else, La sintaxis es

if (condition) {
   // bloque A se ejecuta si condition es true
} else {
  // bloque B se ejecuta si condition es false
}
fun main() {
    val age:Int = 10

    if (age > 18) {
        print("Adult")
    } else {
        print("Minor")
    }
}

La sentencia if.. else es una expresion

Recuerda que una expresión devuelve un valor como un Int, un String etc. La sentencia if..else también es una expresión porque devuelve un valor que para simplificar podemos decir que es el valor que se obtiene del bloque A si la condición es cierta o del bloque B si es falsa. En el siguiente ejemplo, ya que la condición es false y el código B consiste simplente en un literal string “Minor”, el valor asociado al código B, y por tanto a la expresión if..else será “Minor”

fun main() {
    val age:Int = 10

    val result = if (age > 18) {
        "Adult"
    } else {
        "Minor"
    }
    println(result)
}

Uso opcional de los {}

Es frecuente que el código A y B se compongan de tan sólo una instrucción y entonces podemos evitar los {} y ver escrito el ejemplo anterior de forma más compacta

fun main() {
    val age:Int = 10

    val result = if (age > 18) "Adult" else  "Minor"
    println(result)
}

Es importante observar que el if simple no se puede usar como expresión, es obligatoria la existencia del else.

Sobre el valor que devuelve un bloque del if

¿Que pasa si el código de los bloques no se componen de una única instrucción y contiene varias expresiones?. En este caso el valor del bloque será el de la última expresión

fun main() {
    val age:Int = 10

    val result = if (age > 18) {
        println("Hola, la condición es true")
        var y = 2+3 // esto no es la última expresión del bloque A
        "Adult"
    } else {
        println("Hola, la condición es false")
        var z = 10/5 // esto no es la última expresión del bloque B
        "Minor"
    }
    print("Valor asociado al if..else: ")
    println(result)
}

if anidado

Cuando una expresión está presente dentro del cuerpo de otra expresión, se denomina expresión anidada. Dentro de un if o un else puede haber otro if “anidado”.

val x = 37
val y = 89
val z = 6

val result = if (x > y) {
    if (x > z)
        x
    else
        z
} else {
    if (y > z)
        y
    else
        z
}
return result

escalera if

Hay muchas combinaciones para anidar, pero una muy frecuente es anidar dentro un else un if-else y que esto ocurra en multiples niveles de anidamiento. En este caso el código adopta forma de “escalera” y se puede hacer poco legible

fun main() {
    val number = 60

    val result = if (number < 0) {
        "Numero negativo"
    } else {

        if (number < 10){
            "Numero con un único  digito "
        } else {
            if (number < 100) {
                "Número con 2 dígitos"
            } else {
                "número con más de dos digítos"
            }
        }
    }
    print(result)
}

Cuando dentro de cada else la única instrucción es otra if-else anidada y teniendo en cuenta que por lo tanto podemos evitar los {} correspondientes podemos escribir todo más legible suprimiendo los {} del else y pegando la palabra if al else

fun main() {
    val number = 60

    val result = if (number < 0) {
        "Numero negativo"
    } else if (number <10) {
        "Numero con un único  digit "
    } else if (number <100) {
        "Número con 2 dígitos"
    } else {
        "número con más de dos digítos"
    }
    print(result)
}

No es más que un reagrupamiento de lineas pero da la sensación que tenemos una nueva sentencia else if más legible que la escalera inicial

El operador in combinado con if

El operador in se usa para verificar la existencia de un valor dentro de una colección como por ejemplo dentro de un rango o un array. A menudo lo veremos formar parte de la condición de un if com en el ejemplo

fun main() {
    val nombres= arrayOf("chosky","chuly", "uinchi")
    if ("Pepe" in nombres)
        println("Pepe  está en el array")
    else
        println("Pepe  no está en el array")
}
fun main() {
    val edadesPermitidas=10..20
    val miedad=15
    if ( miedad in edadesPermitidas)
        println("Puedes pasar")
}
Última actualización: 23.09.2025

Sentencia when

Al igual que if es una sentencia y también una expresión y por lo tanto devuelve un valor al ser ejecutada. Todo lo que se escribe con when se puede escribir con if else pero hay en situaciones en las que when genera un código más limpio y fácil de leer y se prefiere a su equivalente if else.

Fíjate en el siguiente ejemplo para observar la estructura de esta sentencia. Entre los paréntesis del when va una expresión que finalmente al evaluarla se reducecude a un valor, en el ejemplo, la expresión es una simple variable y su valor es 2. Evaluada la expresión del when, a continuación se examina secuencialmente cada línea que llamaremos en este caso rama. Se avanza pues secuencialmente de rama en rama hasta que se encuentra una cuyo valor coincide con el valor de la expresión anterior. Si se encuentra coincidencia se ejecutan ejecutan exclusivamente las acciones de dicha rama y con esto finaliza la ejecución del when ignorándose el resto de las ramas aun no examinadas. Es típico que la última rama sea un else para que funcione como opción por defecto cuando el resto de las ramas no se cumplen

fun main() {
    val day = 2
    when (day) {
        1 -> println("Monday")
        2 -> println("Tuesday")
        3 -> println("Wednesday")
        4 -> println("Thursday")
        5 -> println("Friday")
        6 -> println("Saturday")
        7 -> println("Sunday")
        else -> println("Invalid day.")
    }
}

a continuación vemos alguna posibilidad sintáctica que se ve muy frecuentemente con when

Agrupar varios valores en una rama

fun main(args: Array<String>) {
   val day = 2

   when (day) {
     1, 2, 3, 4, 5 -> println("Weekday")
     else -> println("Weekend")
   }
}

Usar rangos en las ramas

fun main() {
    val day = 2

    when (day) {
        in 1..5 -> println("Weekday")
        else -> println("Weekend")
    }
}

Usar expresiones en lugar de valores en la parte izquierda de la rama

Realmente como una expresión una vez evaluada se reduce a un valor, las consideraciones son las mismas que cuando escribimos un valor sencillo.

fun main() {
    val x = 20
    val y = 10
    val z = 10

    when (x) {
        (y+z) -> print("y + z = x = $x")
        else -> print("Condition is not satisfied")
    }
}

Una rama puede contener un bloque de código

En este caso, hay que usar llaves para delimitar el bloque

fun main() {
    val day = 2

    when (day) {
        1 -> {
            println("First day of the week")
            println("Monday")
        }
        2 -> {
            println("Second day of the week")
            println("Tuesday")
        }
        3 -> {
            println("Third day of the week")
            println("Wednesday")
        }
        4 -> println("Thursday")
        5 -> println("Friday")
        6 -> println("Saturday")
        7 -> println("Sunday")
        else -> println("Invalid day.")
    }
}

Como when es una expresión, se puede usar su valor.

La rama elegida tendrá a la derecha de -> un valor, expresión o bloque que devuelve un valor. Es el valor que se asocia en su conjunto a toda la expresión when

fun main() {
    val day = 2

    val result = when (day) {
        1 -> "Monday"
        2 -> "Tuesday"
        3 -> "Wednesday"
        4 -> "Thursday"
        5 -> "Friday"
        6 -> "Saturday"
        7 -> "Sunday"
        else -> "Invalid day."
    }
    println(result)
}
Última actualización: 23.09.2025

Bucle for

¿Qué son los bucles?

Imagina una situación en la que necesite imprimir en pantalla una oración 20 veces. Podemos escribir println(oracion) 20 veces, pero, ¿Qué pasa si necesitas imprimir la misma oración mil veces? Aquí es donde necesitamos usar bucles para simplificar el trabajo de programación. En realidad, los bucles se utilizan en la programación para repetir un bloque específico de código hasta que se cumpla una determinada condición. A los bucles también se les llama a menudo por su nombre en inglés loop o el nombre más formal de estructura iterariva. Kotlin admite varios tipos de bucles y en este documento veremos el bucle for.

Sintaxis del bucle for

for (item in colección) { // cuerpo del bucle }

Veremos más adelante que es una colección, por el momento, informalmente, digamos que es una colección de elementos como los rangos, arrays o Strings. El bucle ejecuta el cuerpo tantas veces como elementos tenga la colección, es decir, itera sobre la colección y en cada iteracción o vuelta devuelve un elemento de la colección en la variable . Observa que in es un operador que ya vimos al estudiar rangos y que es un elemento obligatorio en la sintaxis del for.

Iterar sobre un String

En el siguiente ejemplo, en cada iteración se imprime una letra de la palabra “hola”

fun main() {
    for (item in "hola") {
        println(item)
    }
}

Iterar sobre un rango

fun main() {
    for (item in 1..5) {
        println(item)
    }
}

El desplazamiento a través del rango es de uno en uno, es decir, en cada paso incremento el desplazamiento dentro del rango en 1. Con la palabra reservada step puedo indicar otro incremento de desplazamiento.

fun main() {
    for (item in 1..10  step 2) {
        println(item)
    }
}

Puede querer iterar sobre un rango pero comenzado por el último elemento y avanzando descendentemente utilizando la palabra reservada downTo

fun main() {
    for (item in 5 downTo 1 step 2) {
        println(item)
    }
}

Bucles anidados

Estudiamos anteriormente que algunas sentencias como el if se pueden anidar. También es posible anidar bucles. Veamos un ejemplo con el bucle for

fun main() {
    for(i in 1..3)
        for(j in 1..3)
            println("$i$j")
}

Hay un bucle “exterior” controlado por una variable i y otro “interior o anidado” controlado por j. En cada paso del bucle exterior se ejecuta el bucle interior o lo que es lo mismo en este caso para cada valor de i se ejecuta el bucle interior. Tienes que entender a la perfección la salida del código anterior.

En ocasiones se necesitan niveles extra de anidamiento pero es más infrecuente

fun main() {
    for(i in 'a'..'b')
        for(j in 1..2)
            for(k in 'x'..'z')
                println("$i$j$k")
}
Última actualización: 23.09.2025

while y do while

while

sintaxis

while (condition) {
    // body of the loop
}

Mientras la condición sea verdadera se ejecuta el cuerpo del bucle

fun main() {
    var i = 5
    while (i > 0) {
        println(i)
        i--
    }
}

Observa que:

  • a menudo vamos a precisar declarar una variable externa al bucle para controlar la condición de salida, a dicha variable se le suele llamar con el termino contador
  • tenemos que escribir dentro del cuerpo del while como queremos que se incremente/decremente el contador.
  • sería posible que el cuerpo se ejecute 0 veces. Decimos que el cuerpo de un bucle while se puede ejecutar de 0 a n veces

do while

Similar al while con la siguientes diferencias

  • la condición del bucle se escribe y comprueba despues de ejecutar el cuerpo
  • por lo tanto, el cuerpo se ejecuta al menos 1 vez. Decimos que el cuerpo se ejecuta de 1 a n veces

sintaxis

do{
    // body of the loop
}while( condition )
fun main() {
   var i = 5;
   do{
      println(i)
      i--
   }while(i > 0)
}

Bucles infinitos

Son bucles con infinitas iteraciones.

fun main() {
    while(true)
        System.out.println("Hasta el fin de los tiempos....")
}

Aunque ahora pueda resultar sorprendente, los bucles infinitos se utilizan provechosamente en diversas situaciones. También hay que tener en cuenta que podemos generar un bucle infinito de forma no intencionada si escribimos mal el código del bucle haciendo por tanto que el programa se “cuelgue” debido a que jamás se llega a cumplir la condición de fin de bucle, normalmente porque el contador que suele ir en la condición del bucle se incrementa incorrectamente o no se incrementa.

fun main() {
    var i=0
    while(i<6)
        System.out.println("i vale $i. i nunca cambia  y jamás se llega a cumplir que i>= 6 para que pare el bucle")
}

for vs while

En kotlin se usa más el bucle for por muchas razones:

  • for suele generar código más conciso y facil de entender, no es raro ver bucles for escritos en una sóla línea.
  • for es más seguro, por ejemplo, evitan los típicos despistes de incremento/decremento manual de los índices que controlan el bucle
  • tiene muchas posibilidades asociadas a técnicas de programación funcional.

No obstante, siempre habrá un buen momento para usar el viejo while y su hermano do while. Nos encontraremos con problemas que para resolverlos es más natural expresar la solución pensando en mientras que …. Por ejemplo, crear un bucle infinito con while(true) es sencillo y elegante. Irán saliendo otras situaciones y casos en lo que preferiremos el while. Nosotros como estudiantes de programación debemos entender y manejar tanto el for como el while.

Última actualización: 23.09.2025

variables locales y bloques

Variables locales y bloques

Llamemos bloque de instrucciones al conjunto de instrucciones definido entre {}. Al utilizar sentencias condicionales y bucles estos tendrán bloques de instrucciones definidos entre {}. En el siguiente ejemplo observamos que el bloque del cuerpo del while está anidado dentro del bloque de la función main() y observamos como desde el bloque interno se puede acceder a las variables del bloque externo pero no al revés.

fun main() {
   var x=20
   while(x<20){
      x++ //puedo usar una variable declarada en un bloque más externo
      var y= 56
   }
   //println(y) no se puede usar una variable declarada en un bucle interno
}

variables locales de bloques anidados con el mismo nombre

Evita usar variables en bloque externo e interno con el mismo nombre. Es posible hacerlo pero genera confusión al leer el código. En el siguiente ejemplo observamos que la variable x del bloque if oculta a la x del bloque externo en el momento de ejecución del bloque if

fun main() {
   var x=20
   if(x<100){
      var x=8
      println(x)
   }
   println(x)
}

repetir nombres de variables en bloques secuenciales.

Algo que si se utiliza mucho es repetir el mismo nombre para las variables contador de los bucles cuando estos son secuenciales (sin anidar). Así evitamos que tener que estar inventando nombres. En el siguiente ejemplo llamamos siempre i a los contadores. Observa que son variables que se crean y destruyen con cada for y no surgen problemas de ambigüedad.

fun main() {
   for(i in 1..3) println("Hola mundo")
   for(i in 1..3) println("Adios mundo")
}
Última actualización: 23.09.2025

break y continue

son formas de alterar la iteración normal de un bucle que en principio está exclusivamente dirigida por la condición del bucle.

  • break permite finalizar la ejecución del bucle
  • continue permite finalizar la iteración actual del bucle

Tanto break como contine se pueden utilizar con for, while o do while.

sintaxis

El break y el continue suelen ir dentro de un if o estructura condicional. No tendría sentido práctico ejecutar un break/continue sin verificar previamente que se cumple una condición. Vemos la sintaxis sólo con break teniendo en cuenta que con continue la sintaxis es idética.

// Using break in for loop
for (...) {
   if(test){
      break
   } 
}

// Using break in while loop
while (condition) {
   if(test){
      break
   } 
}

// Using break in do...while loop
do {
   if(test){
      break
   } 
}while(condition)

Si la condición test es cierta, se ejecuta el break/continue.

break

Si la condición test es cierta, se ejecuta el break de forma que inmediatamente se para de ejecutar la iteración actual y se sale del bucle. Por lo tanto despues de un break de un bucle la siguiente instrución a ejecutar será la siguiente instrucción al bucle que no pertenece al bucle

fun main() {
   var i = 0
   while (i < 100) {
      println(i)
      if( i == 3 ){
         break
      }
      i=i+1

   }
}

el código anterior imprimirá los números 0, 1,2 y 3 En cambio, el siguiente código

fun main() {
   var i = 0
   while (i < 100) {
      if( i == 3 ){
         break
      }
      println(i)
      i=i+1

   }
}

imprimirá los números 0, 1 y 2 Tienes que entender perfectamente la diferencia de impresión entre estos dos últimos ejemplos

continue

El siguiente programa imprimirá los números 0, 1,2,4 y 5. No imprime el 3.

fun main() {
   var i = 0
   while (i< 6) {
      if( i == 3 ){
         i++
         continue
      }
      println(i)
      i++

   }
}

¿Qué ocurriría sin no hay un i++ dentro del if? ¡Prúebalo y razónalo!

Break y continue funcionan por supuesto con do while y con for. Por ejemplo, el código equivalente al anterior con for podría ser:

fun main() {
   for (i in 0..5){
      if(i==3) continue
      println(i)
   }
}
Última actualización: 23.09.2025

Otras formas de iterar

Hay otras formas de iterar en kotlin que veremos más adelante y que por el momento simplemente nombramos:

  • recursividad.
  • diversas técnicas que utilzan conceptos de programación funcional como las funciones repeat() y foreach() entre otros mecanismos.
Última actualización: 23.09.2025

Funciones

El concepto de función de programación es parejo al de función matemática, pero incorpora diversos matices para adecuar el concepto matemático de función al mundo de la programación.

Ya utilizamos funciones en nuestros ejemplos, por ejemplo la función print() que es una función escrita por los fabricantes de kotlin y que nosotros podemos usar cuantas veces queramos. Además, en nuestros ejemplos no sólo estuvimos usando funciones ya hechas como print(), también escribimos el código de una función, concretamente escribimos una y otra vez el código de la funcion “especial” main() que será una función que llama o invoca el sistema Kotlin para comenzar la ejecución de nuestra aplicación.

Definir y llamar a una función

Una función es un bloque de código que se escribe para realizar una tarea en particular. Se escribe una vez y luego se puede utilizar o llamar las veces que queramos y gracias a esta característica, las funciones son uno de los mecanismos que tienen los lenguajes de programación para evitar duplicar código.

Para poder disfrutar de una función tendremos que hacer dos cosas:

  • definir la función
  • llamar a la función.

Definición de una función

Una función se debe definir según esta estructura básica. A esta estructura básica iremos añadiendo excepciones.

estructura_fun estructura_fun

Ejemplo

función que recibe un número entero y devuelve dicho número elevado al cuadrado.

fun elevarAlCuadrado(x: Int): Int {
    return x * x
}

los nombres de las funciones en kotlin deben seguir las mismas cuestiones de estilo que las variables.

Por lo tanto para definir una función:

  • Comenzamos con la palabra reservada fun
  • Nombre de la función − Es el nombre que eliges para la función con el fin de esclarecer su propósito
  • Lista de parámetros − . Defínelos como nombre:tipo y sepáralos por comas.
  • Tipo de retorno − Tipo de dato de salida de la función.
  • Cuerpo de la función – Son todas las sentencias que realizan la tarea para llegar al resultado final de retorno. Usa la expresión return para devolver el valor.

funcion funcion

Llamar a una función

Parámetros y argumentos

Para entender bien el mecanismos de llamada a una función es necesario tener clara la relación entre el concepto de parámetro y el concepto de argumento, que por cierto, es el mismo que en las matemáticas tradicionales:

  • parámetro. Un parámetro no es más que una variable definida entre los paréntesis de la función.
  • argumento. Un argumento es un valor que se le pasa a una variable parámetro a través del mecanismo de llamada a una función.

En muchos sitios web usan indistintamente estos dos términos y, además de que no es correcto, puede generarte confusión y falta de claridad en las explicaciones.

argumentos argumentos

El mecanismo de llamada a una función

Una vez que tenemos definida una función, podemos llamar o invocar a una función.

LLamar a una función consiste simplemente en escribir su nombre junto a los valores que queremos que tomen sus parámetros. Recuerda que a los valores que le pasamos a la función le llamamos argumentos. En el siguiente ejemplo, definimos la función, y luego en main() pasamos a usarla/llamarla/invocarla las veces que queramos, en este caso sólo dos veces pero no hay límite de cantidad de llamadas.

argumentos argumentos

Con un poco más de detalle, lo que ocurre cuando se llama a una función es:

  1. los argumentos se asignan a los parámetros
  2. se ejecutan las instrucciones de su cuerpo que probablemente utilicen los valores recibidos por los parámetros
  3. y finalmente como resultado de su ejecución devolverán casi siempre un valor de retorno.

Así por ejemplo, cuando realizamos la primera llamada square(2) ocurre:

  1. Al parámetro x se le asigna el valor “2”. Puedes imaginarte que internamente en la función al ser invocada de esa manera ocurre una instrucción de asignación tipo x=2
  2. se hace el cálculo x*x que es 4
  3. como esta expresión está despues de la palabra reservada return, el valor 4 es lo que finalmente “devuelve” la función.

Funciones con Cuerpo De Expresión

También se les conoce por funciones de una sóla línea.

Si el cuerpo de una función es tan sencillo que consiste en devolver simplemente el valor de una expresión podemos escribir la función con una sintaxis más breve. Volvemos a escribir la función square() con esta sintaxis y observamos:

  • se escribe en una única línea
  • desaparecen {} y return.
  • aparece =
fun square(x: Int): Int =  x * x

fun main() {
    println(square(2))
    println(square(5))
}

Retorno tipo Unit

Toda función tiene que tener un tipo de retorno. Si una función no devuelve ningún valor su tipo de retorno es Unit que es un tipo de retorno especial que justamente indica que la función no devuelve nada.

fun saludar(nombre: String): Unit {
    println("Hola, " + nombre)
}
fun main() {
   saludar("Winchi")
}

El tipo de retorno Unit se puede omitir

si en la definición de la función omitimos el tipo de retorno se asume que su tipo es unit.

¿Cuál es el tipo de la famosa función main()? Observarás que no se especifica, por tanto su tipo de retorno es Unit. En el siguiente ejemplo no se especifica el tipo para saludar() y por tanto sabemos que su tipo es Unit, es decir, que no devuelve ningún valor

fun saludar(nombre: String){
    println("Hola, " + nombre)
}
fun main() {
   saludar("Winchi")
}

Unit y return

Cuando una función es de tipo Unit, es decir, no devuelve ningún valor , podemos omitir el return cuando dicho return es la última instrucción de la función. En el siguiente ejemplo añadimos el return a saludar() y vemos que el efecto es el mismo que no ponerlo.

fun saludar(nombre: String): Unit {
   println("Hola, " + nombre)
   return
}
fun main() {
   saludar("Winchi")
}

Pero veremos más adelante, que una función no tiene porque tener un único return colocado como última instrucción, así que, lo indicado anteriormente tendrá matices que veremos en su momento.

Named arguments (argumentos con nombre)

Si al llamar a una función, indicamos el nombre del parámetro, podemos cambiar el orden de los argumentos.

fun imprimirXYZ(x: Int, y: Int, z: Int) {
    println("x: $x, y: $y, z: $z")
}


fun main() {
    imprimirXYZ(1, 2, 3)
    //usar named arguments cambiando orden
    imprimirXYZ(z = 3, x = 1, y = 2)
}

Parametros con valores por defecto

Al definir la función es posible indicar los valores por defecto de los parámetros. Por el momento para simplificar, nos fijamos en el funcionamiento de este mecanismo cuando la función consta de sólo un parámetro.

fun saludar(nombre: String="Churry"){
    println("Hola, " + nombre)
}
fun main() {
    saludar("Winchi")
    saludar()
}

Se debe procurar poner los parámetros con valor por defecto al final (a la derecha). Los valores que se proporcionan en la llamada se empiezan asignar por la izquierda.

fun imprimirXYZ(x: Int, y:Int=2, z:Int=3) {
    println("x: $x, y: $y, z: $z")
}


fun main() {
    imprimirXYZ(7,8,9)
    imprimirXYZ(7,8)
    imprimirXYZ(7)
}

si nos empeñamos en poner primero (a la izquierda) los parámetros con valor por defecto, tenemos que usar argumentos con nombre para llamar la función. En el siguiente ejemplo las llamadas comentadas producen un error de compilación ya que z se queda sin valor.

fun imprimirXYZ(x: Int=1, y:Int=2, z:Int) {
    println("x: $x, y: $y, z: $z")
}


fun main() {
    imprimirXYZ(7,8,9)
    //imprimirXYZ(7,8)// x vale 7, y vale 8, pero z se queda sin valor
    //imprimirXYZ(7)// x vale 7,  y vale 2,  pero z se queda sin valor
    imprimirXYZ(z=9)// x vale 1,  y vale 2,  y z vale 9
}

Variables locales y globales.

  • variable local: variable que se define dentro de una función
  • variable global: variable que se declara al principio del programa (al principio del fichero) fuera de toda función.

En el siguiente ejemplo x es una variable global al programa de forma que es accesible desde todas las funciones del fichero, en este caso fn() y main().

 
var x = 100 // variable global

fun fn() {
   x = x + 100
}

fun main() {
   println("x vale : $x")
   fn()
   println("x vale: $x")
}

A lo largo del curso, iremos entendiendo que, salvo en casos especiales, debe evitarse el uso de variables globales ya que generan código inseguro de mala calidad.

variables de ámbito local a una función.

Las variables que se declaran en una función y su funcionamiento es local a esa función, son:

  • los parámetros, que no son más que un tipo un poco especial de variables locales que toman su primer valor cuando se invoca a la función con argumentos. Aunque ciertamente los parámetros tienen un un ambito local a la función, no uses el término variable local para los parámetros, usa mejor siempre el término parámetro.
  • las variables locales. Son las variables que se declaran en el cuerpo de la función.

Una variable local sólo es accesible desde la función que la declara. En el siguiente ejemplo observa que la variable x sólo es accesible desde fn() y la variable y desde main(). Si descomentas las instrucciones comentadas observarás el error.

fun fn() {
   var x = 100
   println(x)
   //println(y) //error,  y no es una variable conocidad dentro de fn
}

fun main() {
   var y =10
   println(y)
   //println(x) //error, x no es una variable conocida dentro de main
}

Esta idea es extensible a los parámtros con la salvedad de la asignación entre parámetro y argumento. Observa el error si descomentamos en main() la instrucción comentada

fun fn(x:Int) {
   println(x)
   //println(y) //error y no es una variable conocidad dentro de fn
}

fun main() {
   var y =10
   fn(7)
   println(y)
   //println(x) //error, x no es una variable conocida dentro de main
}

Por lo tanto, funciones diferentes pueden tener variables con nombre coincidentes, no hay lugar a la confusión ya que realmente son variables diferentes sólo accesibles desde su función.

fun fn1(x:Int) {
   println(x)
}
fun fn2(x:Int) {
   println(x)
}
fun fn3() {
   var x=3
   println(x)
}
fun main() {
   fn1(1)
   fn1(2)
   fn1(3)
   var x= "x de main"
   println(x)
}

la variable local oculta al parámetro

No tiene sentido práctico inmediato tener en la misma función un nombre de parámetro coincidente con un nombre de variable local, pero es posible. En caso de coincidencia de nombre lo que ocurre es que ambas variables existen, pero la variable local oculta al parámetro en el sentido que las instrucciones de la función usan la variable local, no el parámetro. Observa lo comentado en el siguiente ejemplo y simplemente concluye que aunque no genera error, es mejor no usar el mismo nombre para parámetro y variable local para evitar código confuso dificil de entender.

fun f(x:Int):Int{
   var x=3
   return x
}
fun main() {
   println(f(8))
}

funciones con parámetros tipo lista

Cuando se quiere especificar un parámetro de tipo array lo que simplemente queremos es declarar su tipo y lo hacemos indicando List<tipo>

fun leerLista(miLista: List<Int>) {
    for (num in miLista) print("$num ")
}

fun main() {
    val unaLista = listOf(1, 2, 3)
    leerLista(unaLista)
}

Cuando se pasa una lista como argumento lo que se está pasando es la dirección del array en memoria, no una copia de sus datos, por lo tanto, desde la función el array original es modificable

fun cambiarLista(miLista: MutableList<Int>) {
    for (i in miLista.indices) miLista[i] = 999999
}

fun main() {
    val unaLista = mutableListOf(1, 2, 3)
    cambiarLista(unaLista)
    for (num in unaLista) print("$num ")
}

Cuando estudiemos las listas con un poco más de profundidad que lo visto hasta ahora retomaremos esta cuestión.

Sobrecarga de funciones

Sobrecargar una función consiste en definir una función varias veces con el mismo nombre pero con distinto tipo de argumentos y/o número de argumentos. La sobrecarga permite por tanto que una función se comporte de forma diferente en función de la cantidad y tipo de sus parámetros

fun saludar(nombre: String): Unit{
    println("Hola $nombre")
}
fun saludar(nombre: String, edad:Int): Unit{
    println("Hola $nombre no sabía que tenías $edad años")
}
fun saludar(nombre: String, sueldo:Double): Unit{
    println("Hola $nombre no sabía que ganabas  $sueldo €")
}
fun main(){
     saludar("Chuly")
     saludar("Chuly",35)
     saludar("Chuly",350.0)
}

funciones built-in o integradas.

El término built-in puede tener significados un poco diferentes según el contexto. Para nosotros ahora, una función built-in, integrada, incorporada o standard entre otros nombres se refiere a las funciones que vienen integradas dentro del propio sdk de kotlin y por tanto podemos usarlas de forma inmediata en nuestro código.

Ya que Kotlin es un lenguaje de POO la mayoría de las funciones se pueden usar asociadas a objetos, recuerda las funciones de los objetos String.

fun main() {
    println("Hello World!".reversed())
    println("Hello World!".uppercase())
}

Aunque muchas menos que las anteriores, también hay muchas funciones que pueden usarse sin estar asociadas a ningún objeto o variable referencia. Estas funciones están escritas en un paquete. El concepto de paquete lo veremos más adelante pero por el momento lo podemos asimilar al nombre de una carpeta que contiene el fichero que a su vez contiene la función que me interesa. Un paquete con funciones muy usado es el que contiene funciones matemáticas como abs(), max() etc. Observa en el siguiente ejemplo que precisamos una sentencia import para indicar que función de que paquete es la que queremos usar

import kotlin.math.abs
fun main() {
    //abs() ya está escrita, simplemente la usamos cuando queramos en nuestro código
    //imprimir el valor absoluto de -5
    println(abs(-5))
}

por comodidad podemos importar todas las funciones de un paquete usando el wildcard *

import kotlin.math.*
fun main() {
    println(abs(-5))
    println(max(5, 10))
    println(min(5, 10))
    println(sqrt(25.0))
    //etc.
}

por otro lado, kotlin es compatible con Java lo que quiere decir entre otras cosas que puede usar su libreria de clases. Podemos importar funciones(realmente métodos) de dichas clases. Observa como podemos obtener de la libreria java la función now() para obtener el instante actual

import java.time.Instant.now
fun main() {
    println("instante actual: ${now()}")
}

El paquete standard. ¿Y porque no usamos import para println() o readln()?

El conjunto de funciones y clases proporcionadas por defecto en Kotlin se conoce como el “paquete estándar de Kotlin”. Este paquete contiene un conjunto de funciones y clases que son esenciales para la programación en Kotlin y están disponibles por defecto en cada archivo de código Kotlin sin necesidad de importar nada adicional. El paquete standard de kotlin se llama simplemente ‘kotlin’ y por tanto con ‘kotlin.*’ importaría todo su contenido

import kotlin.* // import everything from the kotlin package
fun main() {
    println("Hello, world!")
}

Pero ten encuenta que la sentencia import anterior es innecesaria pues ya la hace automáticamente kotlin por nosotros.

¿Porqué usar funciones?

La incorporación de las funciones en la programación fue un gran avance, algunas razones:

  • Se pueden ejecutar más de una vez en un programa y/o en diferentes programas ahorrando tiempo de programación.
  • Es una forma de compartir código entre programadores.
  • Es una forma de dividir un problema complejo en problemas simples. cada problema simple sería una función. Esto además facilita la división de tareas entre un equipo de programadores.
  • Mejora la estructura y legibilidad de un programa.
  • Se pueden probar individualmente y por tanto facilita el mantenimiento del programa.
  • Son la base del paradigma de programación funcional que veremos más adelante en el curso.
Última actualización: 23.09.2025

El valor nulo

En estos momentos, el concepto de nulo,null en inglés, puede resultarte un poco desconcertante pero cobrará sentido poco a poco con la práctica .

El valor null

Es un valor especial que se utiliza para indicar justamente que no hay valor. Por ejemplo, si queremos que una variable en un momento dado no almacene ningún valor, le damos el valor null que a pesar de ser un valor tiene el significado especial de “no valor”. No pienses que el valor Null es similar a 0(cero) o blanco o una “cadena vacía”, es un valor muy especial y no se comporta como ninguno de esos valores.

El valor null se usaba mucho en los antecesores de Kotlin como C++ y Java. En Kotlin se intenta limitar este uso y esto conlleva tener claro una serie de cuestiones que abordaremos a continuación.

Kotlin es null safety

Kotlin es Null Safety, es decir, que gestiona los nulos de forma segura, de modo que puedes garantizar facilmente que tu código no va a producir NullPointerException (NPE). ¿Qué es NullPointerException? Por el momento simplemente indicar que es un error que se puede generar al ejecutar un programa que trabaja con nulos. Intenta intuir que es NullPointerException con el siguiente ejemplo. La segunda instrucción por razones que entenderás más adelante genera NullPointerException y ahí se para en seco la ejecución del programa, ninguna de las instrucciones que siguen a la segunda instrucción se van a ejecutar jamas. ¡Pruébalo!

fun main() {
    val x: Int? = null
    val y = x!!.toDouble()
    print("la excepción aborta el programa y nunca se imrprime esto")
    println("ni esto ....")
}

¿Son malas las NPE?

No necesariamente. Depende del contexto y estilo de progrmación. Cuando estudiemos el control de excepciones por un lado y porgramación funcional por otro, entenderemos mejor este problema. Por el momento simplemente indicar que al usar en Kotlin técnicas de programación funcional las NPE se convierten en un engorro y se tuvo esto en cuenta en el diseño de Kotlin.

Por defecto una variable no puede tomar el valor null

Esto por ejemplo no compila

val x: Int = null

Marcar el tipo con ? para permitir nulos

Si quieres que una variable acepte nulos, tienes que marcar el tipo con una ?

val x: Int? = null

Añadir un ? al tipo se le denomina definir un tipo como anulable y permite por tanto que el tipo admita en su rango de valores el valor null que por defecto ningún tipo admite.

Chequeo de nulos en tiempo de compilación

Si permitimos expresamente que una variable tome el valor null, el compilador nos puede obligar a comprobar el nulo antes de hacer algo con esa variable para asegurarse de que no se producirá un NullPointerException.

En el siguiente ejemplo simplemente el valor de x lo intento imprimir y no hay problemas de compilación

fun main() {
    val x: Int? = null
    print(x)
}

Pero, si quiero cambiar el valor de x de Int a Double invocando la función toDouble() se genera error de compilación

 fun main() {
    val x: Int? = null
    val y = x.toDouble()
}

Si kotlin permitiera la ejecución del programa anterior y si además ocurriera que el valor de x fuera null, el programa generaría una excepción NullPointerException. Ya indicamos anteriormente que esto se quiere evitar en Kotlin y por lo tanto en el caso anterior Kotlin ya no deja ejecutar el programa generando un error en la fase previa de compilación.

El operador !!

El método menos seguro para el tratamiento de nulos es simplemente indicar al compilador que evite chequeos en tiempo de compilación respecto a la posibilidad de que se produzca una nullpointerException. Esta opción tiene sentido si:

  • estoy completamente seguro que mi variable nunca va a llegarle un valor null
  • Me da igual si se produce una NullPointerException.

Para indicar al compilador que evite el chequeo usamos el operador !! llamado operador de aserción de no nulo, o sea, que aseguramos al compilador que no se va a producir una NullPointerException y que si se produce asumimos la responsabilidad. En el siguiente ejemplo al escrib ir x!! no se genera error de compilación pues inhibimos el chequeo de nulos. Pero, ya que x vale null se genera en tiempo de ejecución una NullPointerException

fun main() {
    val x: Int? = null
    val y = x!!.toDouble()
}

En los siguientes apartados veremos otros enfoques de tratamiento nulos y llegaremos a la conclusión que el operador !!" es la opción menos segura, ya que lo único que hacemos es deshabilitar la inspección de nulos del compilador, no obstante, por su sencillez, es el enfoque que usaremos en nuestros primeros programas.

Observa en el siguiente ejemplo como efectivamente usando !! puedo ocurrir un nullPointerException indeseado que para la ejecución del programa. Recuerda que si usas !! la responsabilidad del control de nulos pasa a ser total para el programador que es el que tendrá que asegurarse de que un NullPointerException no va a parar su programa

fun main() {
    val x: Int? = null
    val y = x!!.toDouble()
    print("la excepción aborta el programa y nunca se imrprime esto")
    println("ni esto ....")
}

A continuación se profundiza un poco más en el tratamiento de nulos pero con lo visto hasta aquí es suficiente por el momento.

if para hacer un tratamiento seguro de nulos

Ahora el compilador no genera error ya que detecta que el código escrito aunque x puede valor null, si esto ocurre, no se ejecuta x.toDouble() ya que lo hemos prevenido con un if y por tanto el código es seguro

 fun main() {
    val x: Int? = null
    if (x!=null){
        val y = x.toDouble()
    }
 }

El if nos ofrece multiples posibilidades, por ejemplo, otra forma típica de gestionar la situación anterior es que si detectamos que x vale null entonces le damos a y un valor que nosotros consideramos apropiado

fun main() {
    val x: Int? = null
    val y =  if (x!=null)  x.toDouble() else 0.0
    print(y)
}

Expresión de acceso seguro

Recuerda que Kotlin, por razones que entenderas más adelante, hay cierta obsesión por evitar La nullPointerException, otra forma de tratar de forma de vitarla es usar una expresión de acceso seguro que con este nombre un poco aparatosos consiste simplemente en añadir una ? despues del nombre de la variable. Comprueba como compila

 fun main() {
    val x: Int? = null
    var y = x?.toDouble()
    println(y)
    y=9.8
    println(y)
}

Observa que la ? se puede indicar:

  • despues de un tipo para convertirlo en anulable y permite que una variable de ese tipo pueda tomar el valor null
  • despues de una variable en una expresión de la forma variable?.metodo. Si resulta que que la variable puede tomar el valor null, ocurre por tanto que la expresión en principio generaría una nullPointerException, pero el efecto de la interrogación es evitar esta excepción y que el valor que devuelva la expresión sea null

En el ejemmplo, como x es null y no especificamos el tipo de de y, el tipo se infiere de la expresión de la derecha y por tanto y se va a crear con tipo Double?, es decir admite valores Double pero también null

¿Cuál es entonces la diferencia entre x?.toDouble y x!!.toDouble()? Si pruebas los ejemplos anteriores observas que:

  • x?.toDouble, si x vale nulo esta expresión devuelve null
  • x!!.toDouble(), si xa vale null esta expresión genera una NPE.

El operador Elvis

Se le llama operador elvis al operador ?: que nos permite completar la sintaxis de la expresión de acceso seguro para matizar, cuando nos interese, que en lugar de devolver nulo devuelva un valor concreto, por ejemplo, ahora si x vale null entonces a y le asignamos 0.0

fun main() {
    val x: Int? = null
    var y = x?.toDouble()?:0.0
    println(y)
    y=9.8
    println(y)
}

Si te fijas

var y = x?.toDouble()?:0.0

no es más que la forma abreviada de

 var y =  if (x!=null)x.toDouble() else 0.0

¡compruébalo!

Última actualización: 23.09.2025

Entrada de datos por teclado con readLine()

Las funciones readLine() y readln()

readline() y readln() son funciones que se pueden utilizar para leer líneas del teclado entre otras funciones. La función readln() es de existencia más reciente, aparece a partir de Kotlin 1.6, por tanto, sólo se puede utilizar con las versiones más recientes de kotlin. Para programas sencillos e iniciarse es mejor usar readln() que oculta el problema del valor null. No obstante, exponemos el mecanismo de lecturra de teclado con readLine() porque por el momento quizá nos veamos forzados a trabajar en un ordenador con versiones anteriores a 1.6 de kotlin y porque todavía se utiliza mucho en los ejemplos que consultamos en la web.

readLine() devuelve String?

readLine() devuelve una línea del teclado y la devuelve como un String al programa, pero además, readLine() también puede devolver null y por tanto decimos que readLine() devuelve un String?. Consulta los apuntes del valor null si no sabes lo que es String?

Fíjate como en la documentación oficial de kotlin nos indica que efectivamente esta función devuelve un String?

readline1 readline1

Comprueba esta afirmación observando el error de compilación del siguiente código

fun main() {
    print("teclea una frase y te la repito: ")
    val x: String = readLine()
    print(x)
}

El problema es que por defecto en una variable de tipo String no se pueden almacenar nulos pero readLine() podría devolver null.

Usando como entrada Standard el teclado no se generan nulos pero todo esto rollazo es debido a que readLine() se puede usar con otras entradas, por ejemplo para leer ficheros.

Usar una variable que permita almacenar null.

Una solución es permitir que x almacene null usando el operador ?

fun main() {
    print("teclea una frase y te la repito: ")
    val x: String? = readLine()
    print(x)
}

¿Qué pasa si no especificamos el tipo de una variables inicializada con readLine()?.

Observa que en el siguiente código no especificamos el tipo de x y compila correctamente

fun main() {
    print("teclea una frase y te la repito: ")
    val x = readLine()
    print(x)
}

Para entender porqué compila, simplemente, hay que tener en cuenta que readLine() devuelve algo de tipo String? y por tanto kotlin infiere automáticamente que x es del tipo String?, es decir que

 val x = readLine()

es equivalente a

val x: String? = readLine()

Esto puedo ser un buen ejemplo de que por un lado cuando no declaramos el tipo de las variables ganamos limpieza y concisión en el código pero en casos como este puede encubrir detalles importantes a tener en cuenta en el resto del código, como no ser conscientes que readLine() no devuelve String, realmente devuelve es String?

Indicar con !! que sabemos que no vamos a recibir null

ya que la entrada por teclado no genera nulos, nos podemos librar de declarar tipos con ? usando el operador !! que como ya vimos al estudiar el valor null, simplemente indica al compilador que no haga el chequeo de posible null.

fun main() {
    print("teclea una frase y te la repito: ")
    val x: String = readLine()!!
    print(x)
}

Hay muchas otras formas de afrontar el hecho de que readLine() devuelve String?. Pero por el momento usaremos la más fácil que es usar como en el ejemplo anterior el operador !!

readln() es equivalente a readLine()!!

Desde Kotlin 1.6, para hacer kotlin más fácil a los principiantes, es posible “ocultar” el problema de devolución de nulos de readLine() simplemente usando la función readln() cuyo funcionamiento es equivalente a usar readLine()!! Fíjate como en el ejemplo anterior simplemente sustituimos readLine()!! por readln() y todo funciona igual.

fun main() {
    val linea = readln()//amtes usamos readLine()!!
    val lista= linea.split(' ')
    var suma=0
    for( numero  in lista){
        suma=suma+ numero.toInt()
    }
    println("La suma  es $suma")
}
Última actualización: 23.09.2025

más sobre colecciones básicas

Previamente comentamos aspectos fundamentales de Strings y algo de listas muy superficialmente. Añadimos ahora el estudio de las colecciones básicas de kotlin: arrays, listas, rangos y mapas. Estos recursos nos permitirá resolver nuevos retos algorítmicos que serían muy difíciles o imposibles de resolver sin estas estructuras.

Última actualización: 23.09.2025

Subsecciones de más sobre colecciones básicas

Arrays

Un array es un conjunto de valores del mismo tipo y permite acceder a ellos a través de una sola variable. Por ejemplo si tengo que almacenar 100 números enteros en lugar de crear 100 variables individuales puedo gestionar esos 100 valores enteros de forma simple y uniforme con un array de enteros a través de una única variable.

Otras cuestiones que caracterizan a un array son:

  • es una estructura de tamaño fijo, una vez que se crea no se puede modifica su tamaño.
  • sus elementos se almacenan de forma contigua.
  • a cada elemento se le asocia un índice correspondiente a su posición teniendo en cuenta que la primera posición se corresponde con el índice 0.

Por ejemplo, esto sería la visión gráfica de un array de 6 elementos de tipo Char arraychar arraychar

La potencia de objetos como el array se entenderá cuando estudiemos estructuras de control. Vemos en este documentos sólo algunas de las cuestiones básicas del manejo de arrays

Crear un Array

Hay varias formas de crear una array, para simplificar, por el momento nos limitamos a crear un array de dos formas:

  • con arrayOf()
  • con arrayOfNulls()

Crear un array con arrayOf()

Simplemente separamos por comas una lista de los elementos que va a almacenar el array. Recuerda que todos los elementos tienen que ser del mismo tipo. El tamaño del array se deduce automáticamente del tamaño de la lista indicada.

val nombres= arrayOf("yo","tú","el")//array de strings
val impares= arrayOf(1,3,5)//array de enteros

Crear una array con arrayOfNulls()

Puede ocurrir que al principio del programa no sepamos los valores del array ya que por ejemplo se quieren introducir los valores por teclado, en este caso, podemos crear el array de forma que contenga sus elementos inicializados al valor null. Basta en este caso simplemente indicar el tamaño deseado y el tipo de los elementos.

 val impares= arrayOfNulls<Int>(3)

Tamaño de un array

Ya indicamos que el tamaño o longitud del array se determina en el momento de su creación.Podemos consultar el tamaño de un array a través de la propiedad size

fun main() {
    val nombres= arrayOf("yo","tú","el")
    val impares= arrayOf(1,3,5,7)
    println("tamaño de array nombres:" + nombres.size)
    println("tamaño de array impares:" + impares.size)
}

Acceder a un elemento del array

Los arrays son accesibles con un sistema de índices similar al que vimos con los Strings de forma que el primer elemento se corresponde con el índice 0, el segundo con el 1, etc. Al igual que con los Strings el acceso a los elementos del array se realiza indicando el índice entre corchetes. Por ejemplo, comprobamos que efectivamente con arrayOfNulls inicializamos a Null y a continuación cambiamos de valor los elementos del array

fun main() {
    val impares= arrayOfNulls<Int>(3)
    println(impares[0])
    println(impares[1])
    println(impares[2])

    impares[0]=55
    impares[1]=99
    impares[2]=33

    println(impares[0])
    println(impares[1])
    println(impares[2])

}

Los arrays tienen un gran número de cuestiones que tratar, no obstante, ya que esto es un curso introductorio preferimos por diversas razones utilizar listas en lugar de arrays. A continuación estudiaremos el concepto de lista en Kotlin.

Última actualización: 23.09.2025

Listas

Una lista, de forma simplificada, es una evolución mejorada de un array. La característica más básica de un array es su acceso por posición a cada elemento individual a traves de [] y esta característica también es posible trabajando con listas como vemos en el siguiente ejemplo

fun main() {
    //val miLista: List<Int> = listOf(1, 2, 3)//el tipo en este caso lo puede inferir el compilador
    val miLista = listOf(1, 2, 3)
    println("imprimir toda la lista junta $miLista") // [1, 2, 3]
    println("imprimir la lista elemento a elemento ")
    println(miLista[0])
    println(miLista[1])
    println(miLista[2])
    println("el tamaño de la lista es:  "+ miLista.size)
}

Algunas diferencias importantes entre listas y arrays

Hay dos tipos básicos de listas:

  • inmutables
  • mutables. Permiten modificar el valor de sus elementos así como añadir/borrar elementos a la lista, es decir, modificar el tamaño de la lista.

Un array es una mezcla de los comportamientos anteriores. Su tamaño se fija en el momento de su creación y no se puede cambiar pero cada elemento individual puede cambiar en cualquier momento.

El ejemplo anterior de listas se corresponde con una lista inmutable, para lo cual utilizamos la función listOf(). A continuación veremos un ejemplo de lista inmutable.

Ejemplo de lista mutable

Hay varias formas de crear una lista mutable, Vemos un ejemplo con mutableListOf()

fun main() {
    val colorsList = mutableListOf("Amarillo", "Azul", "Rojo")

    colorsList.add("Verde") // [Amarillo, Azul, Rojo, Verde] //inserta al final
    colorsList.add(0, "Blanco") // [Blanco, Amarillo, Azul, Rojo, Verde]//inserta en la posición indicada indicada
    colorsList.removeAt(2) // [Blanco, Amarillo, Rojo, Verde]
    //observa como modificamos con []
    colorsList[1] = "Negro" // [Blanco, Negro, Rojo, Verde]
    println(colorsList)
    println(colorsList[0])
}

declarar una lista mutable de tamaño 0 (vacía)

Podemos querer ir construyendo una lista partiendo de una lista vacia. Al partir de una lista vacía Kotlin no puede inferir el tipo de la lista. La solución es incluir el tipo en la declaración de la lista de alguna manera como en el ejemplo.

fun main(){
    var lista= mutableListOf<Int>()//lista de Int de tamaño 0
    println(" tamaño lista ${lista.size}")
    lista.add(99)
    println(" tamaño lista ${lista.size}")
}

Listas vs Arrays

Se prefieren las listas. Las listas tienen características actualizadas de seguridad y permiten una programación más cómoda y legible. Entonces, ¿porqué existen arrays en kotlin?

  • Los arrays pueden ser más eficientes. los arrays garantizan un almacenamiento de los datos de forma contigua en memoria. Esto los hace más eficientes pero hoy en día esto sólo tiene impacto en aplicaciones muy concretas.
  • Kotlin es compatible con Java. En java los arrays son muy importantes.

La funcion split() de los Strings

split() permite trocear o dividir un String en trocitos más pequeños y estos trozos los devuelve en un lista. Como parámetro se le indica el criterio de división o delimitador. Por ejemplo el delimitador en el siguiente ejemplo es el String “:”

fun main() {
    val str = "A:B:C:que bonito:z zz"
    val delim = ":"

    val list = str.split(delim)

    println(list)    // [A, B, C, que bonito, z zz]
}

El delimitador realmente es una expresión regular pero de momento con pensar que es un caracter no es suficiente.

Uno de los usos más frecuentes es querer dividir un texto en palabras utilizando como delimitador el espacio en blanco.

fun main() {
    val str = "Había una vez un circo que alegraba siempre la ilusión"
    val delim = " "

    val list = str.split(delim)

    println(list)    // [Había, una, vez, un, circo, que, alegraba, siempre, la, ilusión]
}

Utilizaremos split() para com combinar con el readln() para conseguir un estilo de entrada de datos por teclado que veremos más adelante.

Asignaciones entre variables Lista

Se explica este concepto con variables tipo lista pero es igualmente aplicable a variables tipo array.

Una variable Lista no almacena directamente los datos de la lista si no que almacena la dirección de memoria donde están almacenados los datos

fun main() {
    var a = mutableListOf(0,2,4,6,8)
    var b = mutableListOf(1,3,5,7,9)
    println(a)
    println(b)
}

La situación en memoría podemos imaginarla como: varlista1 varlista1

Observa ahora la aparición de una nueva variable c

fun main() {
    var a = mutableListOf(0,2,4,6,8)
    var b = mutableListOf(1,3,5,7,9)

    var c =a
    a=b

    println(a)
    println(b)
    println(c)
}

La situación en memoría podemos imaginarla como: varlista2 varlista2

Conclusión: Una asignación entre variables lista no provoca que se copie la lista, si no que la variable de la izquierda también referencia a la misma lista que la variable de la derecha.

Ordenar Listas

Es muy habitual tener una lista y querer ordenarla. Ordenar una lista es un tema más complejo de lo que aparenta y nosotros por el momento nos limitamos a ordenar listas de tipos básicos como Int y Strings de forma ascendente/Descente por “su orden natural”. Para este ordenamiento básico podemos usar la función:

  • sort()/sortDescending(). Entonces el orden se aplica sobre la lista original
  • sorted()/sortedDescending(). Entonces la función devuelve una nueva lista ordenada.l.
//ejemplo con sorted()
fun main(){
    val lista= listOf(4,2,99,7,12)
    var listaOrdenada=lista.sorted()
    println(" lista de enteros ordenada $listaOrdenada")
    println(" lista de enteros antigua está sin ordenadar $lista")
    listaOrdenada=lista.sortedDescending()
    println(listaOrdenada)
    val listaStringsOrdenados= listOf("zalamero","cielo","azul").sorted()
    println(listaStringsOrdenados)
    println(listaStringsOrdenados.sortedDescending())
}
//ejemplo con sort()
fun main(){
    //val lista= listOf(4,2,99,7,12) // para sort() tiene que ser mutable
    val lista= mutableListOf(4,2,99,7,12)
    lista.sort()
    println(lista)
}

Listas de dos dimensiones

Hasta ahora trabajamos con Listas/Arrays unidimensionales y accedíamos a sus elementos a través de un índice.

Una lista de dos dimensiones se puede crear mediante la creación de una lista de listas. Por ejemplo:

val lista2D = listOf(
        listOf(1, 2, 3),
        listOf(4, 5, 6),
        listOf(7, 8, 9)
    )

En este ejemplo, hemos creado una lista de tres elementos, donde cada elemento es una lista de tres números enteros. Esta estructura se puede visualizar como una matriz de 3 filas y 3 columnas, con los siguientes valores:

1 2 3
4 5 6
7 8 9

Por lo tanto, aunque realmente una lista de listas consiste en y se almacena como una lista donde cada uno de sus elementos es a su vez otra lista, para resolver muchos problemas de programación es más conveniente visualizar la lista de listas como una tabla y acceder a cada elemento con dos índices teniendo en cuenta que el primer índice representa la fila de la tabla y el segundo índice representa a la columna de la tabla. En el siguiente ejemplo imprimimos la diagonal de la tabla

fun main(){
    val lista2D = listOf(
        listOf(1, 2, 3),
        listOf(4, 5, 6),
        listOf(7, 8, 9)
    )
    val fila0Col1 = lista2D[0][0] // Devuelve 1
    val fila1Col1 = lista2D[1][1] // Devuelve 5
    val fila2Col2 = lista2D[2][2] // Devuelve 9
    println("$fila0Col1 , $fila1Col1 , $fila2Col2")

}

Listas de dos dimensiones mutables

Crear una lista de dos dimensiones vacia y que vaya aumentando de tamaño a medida que lo necesitemos.

fun main() {
    val tablero = mutableListOf<MutableList<String>>()
    var fila= mutableListOf("00","01","02")
    tablero.add(fila)
    fila= mutableListOf("11","11","12")
    tablero.add(fila)
    println(tablero)
    println("-----------")
    println(tablero[0])
    println(tablero[1])

    println("-----------")
    tablero[0][0]="99"
    tablero[1][2]="88"
    println(tablero[0])
    println(tablero[1])
    println(tablero[1][1])

    println("-----------")
    tablero[0].add("03")
    println(tablero)

} 

listas de más de dos dimensiones

Las ideas aquí explicadas se pueden extender a cualquier número de dimensiones. Por lo tanto, al respecto de número de dimensiones podemos clasificar las listas/arrays en:

  • unidimensionales (se definen con una dimensión)
  • multidimensionales (se definen con más de una dimensión)
Última actualización: 23.09.2025

Rangos

Un rango en Kotlin es tipo que engloba un conjunto de valores que representa el concepto matemático de intervalo de valores. Es decir, es un subconjunto de elementos comprendidos entre un extremo inferior a y un extremo superior b.

Por ejemplo, el rango [0,5] representa los valores enteros del 0 al 5. Para crearlo se usa la función operador toRange()

fun main() {
    val fromZeroToFive = 0.rangeTo(5)
    println(fromZeroToFive) // 0..5
}

Otra sintaxis alternativa y muy usada en la práctica es el formato (a..b) .

fun main() {
    val fromZeroToFive = 0..5
    println(fromZeroToFive) // 0..5
}
Última actualización: 23.09.2025

Mapas

El mapa de Kotlin es una colección de pares clave/valor, donde cada clave es única y solo se puede asociar con un valor. Sin embargo, el mismo valor se puede asociar con varias claves. Podemos pensar en un mapa como la típica tabla en la que la primera columna almacena claves y la segunda columna los valores asociados a las claves. Cada fila refleja la asociación entre una clave y un valor. En el siguiente ejemplo represento un mapa con clave telefono y valor nombre. Observa que las claves són únicas pero los nombres no tienen porqué.

Telefono Nombre
111 Pepe
222 Julieta
333 Romeo
444 Pepe
555 Chuly

Un mapa de Kotlin puede ser mutable ( mutableMapOf ) o de solo lectura ( mapOf ).

Los mapas también se conocen como diccionarios o matrices asociativas en otros lenguajes de programación. Por ejemplo en Python a los mapas se les llama diccionarios.

Crear mapas

Para crear un mapa hay multiples sintácticas. La más sencilla es con mapOf para un mapa inmutable y mutableMapOf para mutable y en ambos casos relacionando cada clave con su valor con el operador to

fun main() {
    val miMapaInmutable = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
    println(miMapaInmutable)

    val miMapaMutable = mutableMapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
    println(miMapaMutable)
}

Acceder por clave a un valor

la operación más importante al trabajar con un mapa es dada una clave acceder a su valor. Si la clave proporcionada no existe el valor asociado será null. La clave se puede especificar con el metodo get() o con [].

fun main() {
    val miMapa = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
    var valorde111= miMapa[111]
    println(valorde111)
    valorde111=miMapa.get(111)
    println(valorde111)
    println(miMapa[9999])//no existe esta clave

    val mapaMutable= mutableMapOf(111 to "PepeMutable",222 to "JulietaMutable")
    valorde111=mapaMutable[111]
    println(valorde111)
    valorde111=mapaMutable.get(111)
    println(valorde111)
}

Modificar un valor en mapa mutable

Simplemente asignamos el nuevo valor a la clave

fun main() {
    val m= mutableMapOf(111 to "Yo",222 to "XX")
    println(m)
    m[222]="Tú"
    println(m)
}

Añadir un valor a un mapa mutable

Si al usar el operador de asignación como en el caso anterior, la clave no existe se asume que queremos insertar un nuevo elemento.

fun main() {
    val m= mutableMapOf(111 to "Yo",222 to "Tú")
    println(m)
    m[333]="El"
    println(m)
}

Partir de un mapa mutable vacío e ir añadiendo elementos

fun main() {
    val m= mutableMapOf<Int, String>()
    println(m)
    m[111] = "Pepe"
    m[343]="chuly"
    println(m)
}
Última actualización: 23.09.2025

Iterar sobre colecciones

Es un tema muy extenso con muchos matices y posibilidades que se irán incorporando y analizando a lo largo del curso. Por el momento vemos las posibilidades más básicas. Sobre strings y rangos ya las habíamos visto previamente para las incorporamos también aquí por completitud.

recuerda la sintaxis del bucle for

for (item in colección) { // cuerpo del bucle }

como ves el for en kotlin es una estructura pensada directamente para iterar sobre colecciones y será la que analizaremos en este documento. También puede iterarse con un bucle while a través del manejo de índices pero suele preferirse con for.

Iterar sobre un String

En el siguiente ejemplo, en cada iteración se imprime una letra de la palabra “hola”

fun main() {
    for (item in "hola") {
        println(item)
    }
}

Iterar sobre un rango

fun main() {
    for (item in 1..5) {
        println(item)
    }
}

El desplazamiento a través del rango es de uno en uno, es decir, en cada paso incremento el desplazamiento dentro del rango en 1. Con la palabra reservada step puedo indicar otro incremento de desplazamiento.

fun main() {
    for (item in 1..10  step 2) {
        println(item)
    }
}

Puede querer iterar sobre un rango pero comenzado por el último elemento y avanzando descendentemente utilizando la palabra reservada downTo

fun main() {
    for (item in 5 downTo 1 step 2) {
        println(item)
    }
}

Iterar sobre un array

Es raro ver un array y que de alguna manera no sea manipulado por un bucle. Recorrer un array es una operación muy frecuente.

Iterar sin preocuparnos de los límtes del array

La forma más sencilla de recorrer un array de principio a fin consiste simplemente en dejar que el operador in detecte automáticamente el fin del array.

fun main() {
    var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")

    for (item in fruits) {
        println(item)
    }
}

Es un método de recorrido limpio y cómodo pero sólo vale para cuando queremos leer el contenido del array de principio a fin. Observa que por ejemplo, si a medida que recorremos el array queremos modificarlo no es posible de una manera sencilla y directa ya que la forma fácil de modificar un array es

array[pos]=valor

es decir, necesitamos el índice de posición para modificar.

Iterar basándonos en un rango de índices

Para multitud de situaciones algorítmicas, vamos a necesitar de alguna manera manejar los índices del array para recorrerlo. Por el momento fíjate simplemente en diversas formas de recorrer con índices basándonos en un rango. Ya irás descubriendo la potencia e importancia de trabajar con índices sobre un array poco a poco.

En el rango indicamos el índice en que queremos comenzar y con el que queremos acabar. Un comienzo muy habitual es el índice 0 y un final muy habitual es lastIndex, o expresado de otra forma, size-1 pero no tienen que ser estos obligatoriamente.

fun main() {
    var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")

    for (index in 0..fruits.lastIndex) {
        print(fruits[index]+ "  ")
    }
    println()
    //idem con size-1
    for (index in 0..fruits.size-1) {
        print(fruits[index]+ "  ")
    }
}

Si el rango que indicamos va desde 0 hasta el último como en los ejemplos anterior, tenemos la posibilidad de acceder a este rango a través de la propiedad indices

fun main() {
   var fruits = arrayOf("Orange", "Apple", "Mango", "Banana")
   
   for (index in fruits.indices) {
      println(fruits[index])
   }
}

Iterar sobre una lista

En lo básico, idem que lo visto para arrays. En el siguiente ejemplo simplemente sustituimos en el ejemplo anterior arrayOf() por listOf

fun main() {
    var fruits = listOf("Orange", "Apple", "Mango", "Banana")

    for (index in fruits.indices) {
        println(fruits[index])
    }
}

Iterar sobre un mapa

Hay mil formas, pero la más sencilla consiste en utilizar un par de variables de la forma (x,y) de forma que en cada iteración x reciba la clave e y el valor como en el siguiente ejemplo.

fun main() {
    val miMapa = mapOf(111 to "Pepe",222 to "Julieta",333 to "Romeo",444 to "Pepe",555 to "Chuly")
    //recorrer el mapa con propiedad key y value
    for ((key, value) in miMapa) {
        println("Clave: $key Valor: $value")
    }


}

Más adelante, estudiaremos más en profundidad la desestructuración kotlin. La desestructuración en Kotlin es una característica que permite descomponer una estructura de datos en partes individuales y asignarlas a variables separadas en una sola declaración como hicimos en el ejemplo de arriba.

Última actualización: 23.09.2025

Programación orientada a objetos

Estudiamos los fundamentos de la programación orientada a objetos sin entrar en detalles de diseño y patrones. Nos centramos sobre todo en aspectos sintácticos y en esta primera versión de los apuntes se asume que el alumno tiene conocimientos de POO en java

Última actualización: 23.09.2025

Subsecciones de Programación orientada a objetos

Objetos y clases

Objetos y Clases

En kotlin todo es un objeto, los datos Int realmente son objetos, las funciones son objetos, etc. Por lo tanto, ya estuvimos utilizando objetos, objetos que pertenecen a clases del sistema Kotlin. En este cuaderno nos introducimos al uso de objetos que son instancias de clases escritas por nosotros mismos.

Una primera visión de lo que es un objeto

Los objetos almacenan datos usando para ello propiedades val/var y pueden contener en su interior definición de operaciones que probablemente utilizan los datos anteriores para hacer algo.

Algunas definiciones para ir arrancando:

  • Una clase: Define en su interior propiedades y funciones. A las clases también se les llama tipos datos definidos por el usuario. Así tenemos los tipos propios del lenguaje como Int y String y los que va definiendo en sus aplicaciones el usuario como la clase Coche, Persona etc..
  • Miembro: Una clase contiene en su interior miembros. Hay dos tipos principales de miembros a los que ya aludimos: propiedades y y funciones.
  • En kotlin una función se puede definir dentro de una clase o fuera de toda clase y según este criterio hay dos tipos de funciones
    • funciones top-level. Las que se escriben directamente en el fichero "al top level del fichero" fuera de toda clase.
    • funciones miembro. Se escriben dentro de una clase y su funcionamiento está ligado a un objeto específico de la clase.
  • Crear un objeto: Como punto de partida y sin ningún rigor digamos que crear un objeto consiste en hacer un val o var al nombre de una clase. A los objetos también se les llama instancia de una clase.

DEFINICIÓN DE UNA CLASE

En general la fexibilidad de Kotlin redunda en multitud de posibilidades sintácticas para escribir lo mismo. Aquí recogemos sólo las posibilidades más fundamentales.

Una clase es una plantilla o modelo que se utilizará para crear objetos. Una clase de Kotlin se define usando la palabra clave class. El cuerpo de una clase puede contener propiedades y/o funciones miembro. En otro cuaderno afinaremos el concepto de propiedad (property). Como punto de partida podemos ver una propiedad como una variable que pertenece a la clase.

La declaración básica de un clase consta del nombre de la clase y el cuerpo de la clase rodeado de llaves.

    class Persona{
      var nombre = ""
      var edad = 0
      fun printMe() {
          print(nombre+ " " + edad)
       }
    } 

Tanto el encabezado como el cuerpo son opcionales; si la clase no tiene cuerpo, se pueden omitir las llaves. La siguiente es una declaración válida de clase. Una clase vacia que se define sólo con la palabra reservada class seguida de un nombre.

class miClaseVacia

CREACIÓN DE OBJETOS

Los objetos se crean a partir una clase que funciona a modo de plantilla o molde. "El molde" describe propiedades y comportamientos. Por lo tanto, todos los objetos de una clase tendrán la misma estructura de propiedades y comportamientos.

La sintaxis más básica para crear un objeto de una clase es:

var varName = ClassName()

Podemos acceder a las propiedades y métodos de una clase usando el operador punto "."

var varName = ClassName()

varName.property = valor

varName.functionName()

En el siguiente ejemplo creamos un objeto de la clase Persona y utilizamos sus miembros con el operador .(punto)

class Persona{
  var nombre = ""
  var edad = 0
  fun printMe() {
      print(nombre+ " " + edad)
   }
} 
val p = Persona() 
p.nombre="yo"
p.edad=14
p.printMe() 

Podemos crear objetos de una clase vacía aunque raramente esto tenga utilidad

class MiClase
fun main(){
    val x= MiClase()
    println(x)
}

La referencia this

this no es más que una variable automáticamente creada por el sistema para cada objeto y que se utiliza para referenciar al propio objeto dentro del propio objeto. Se puede usar la variable this para referenciar a los miembros de un objeto desde dentro de ese objeto. El siguiente ejemplo es equivalente al anterior, la única diferencia es que desde printMe() para aludir a los propiedades también usamos la referencia this.

class Persona{
  var nombre = ""
  var edad = 0
  fun printMe() {
      print(this.nombre+ " " + this.edad)
   }
} 
val p = Persona() 
p.nombre="yo"
p.edad=14
p.printMe()

Usar o no usar this

Los dos últimos ejemplos son equivalentes, entonces, ¿se debe usar this o no?. Es una cuestión de estilo, y en el caso del lenguaje Kotlin, cuando this no es necesario, se prefiereno usarlo en aras de la limpieza y concisión objetivo del estilo kotlin.

Y dicho esto, hay diversas situaciones en que el uso de this es necesario y las iremos examinando a medida que lo requiramos.

Última actualización: 23.09.2025

Constructores

Constructores Kotlin

Un constructor es una función miembro especial que se invoca automáticamente cuando se crea un objeto de la clase. Su objetivo principal es inicializar propiedades y otras variables. Una clase debe tener un constructor y, si no declaramos ningún constructor, el compilador genera un constructor predeterminado.

Kotlin tiene dos tipos de constructores:

  • Constructor principal o primario
  • Constructor secundario

Una clase en Kotlin puede tener como máximo un constructor principal y uno o más constructores secundarios. El constructor principal se utiliza cuando simplemente queremos hacer asignaciones de valores a las propiedades. El constructor secundario se usa cuando se requiere hacer las inicializaciones con algo de lógica adicional.

El constructor predeterminado

En el ejemplo que sigue, la clase Persona no define ningún constructor y por tanto se puede utilizar el constructor predeterminado para crear un objeto de esa clase. El constructor predeterminado se invoca con el nombre de la clase seguido de parentesis vacios.

class Persona{
    val nombre:String="yo"
    val edad:Int=22
}
val p=Persona()//usando constructor predeterminado

print("soy ${p.nombre} y tengo ${p.edad} años")

El constructor principal

Hay unas cuantas variantes sintácticas a la hora de escribir un constructor principal. Nos centramos en lo más básico.

El constructor principal se escribe simplemente indicando despues del nombre de la clase la palabra clave constructor, y a continuación entre paréntesis, un conjunto de parámetros separados por comas.

Lo más habitual y simple es que los parámetros sean variables. La definición de las variables puede hacerse con val/var o sin val/var. Si queremos que los parámetros definan al mismo tiempo propiedades es necesario hacerlo con var/val Es decir, en este caso el uso de var/val convierte a un parámetro variable en propiedad de la clase. Los parámetros también pueden ser funciones pero no ejemplificamos por el momento este caso para simplificar.

class Persona constructor(var nombre:String, var edad:Int){
   //nombre y edad son propiedades definidas en el constructor principal
   var email="chuchy@gmail.com" //propiedad no definida en constructor principal
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15)
println(p1.nombre)//nombre es una propiedad y utilizamos el punto para acceder a ella
println(p1.email)//email también es una propiedad aunque se declaro fuera del constructor principal
p1.printMe() 
val p2 = Persona("tu",35) 
p2.
p2.printMe() 

La palabra clave constructor se puede omitir si no hay anotaciones o modificadores de acceso especificados como público, privado o protegido.

class Persona (var nombre:String, var edad:Int){
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

Se pueden inicializar los parámetros del constructor con valores predeterminados.

class Persona (var nombre:String, var edad:Int=90){
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo") 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

parametros que no son propiedades

Sí no indicamos en los paréntesis del constructor los parámetros con val/var entonces es que preferimos definir dentro de la clase las propiedades y entre los paréntesis simplemente se indican variables que no tienen el rango de propiedades de la clase.

class Persona (elnombre:String, laedad:Int){
   var nombre=elnombre
   var edad=laedad 
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

Bloques init

El constructor principal simplemente tiene capacidad para hacer asignaciones de valores a variables. Podemos añadir lógica a la inicialización con un bloque init. Puede haber más de un bloque de inicialización durante la inicialización de una instancia, los bloques de inicialización se ejecutan en el mismo orden en que aparecen en el cuerpo de la clase.

class Persona (var nombre:String, var edad:Int){
   init{
       if(edad<0) edad=0
   } 
   init{
       println("Segundo init")
   }   
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",-15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

un uso típico de this

Indicamos que progresivamente iran saliendo casos que precisan el uso de this. Un caso muy habitual es que coincidan los nombres de los parámetros con los nombres de las propiedades. En este caso, el nombre del parámetro oculta al de la propiedad por lo que para referirnos a la propiedad debemos de utilizar la palabra reserva this.

En el siguiente ejemplo decidimos definir las propiedades dentro de la clase, no através de los parámetros. Observa que los parámetros del constructor no llevan var/val. Además, también a proposito, decidimos que coincidan los nombres de los parámetros y las propiedades. Inevitablemente, necesitamos usar this para referirnos a las propiedades.

class Persona (nombre:String,edad:Int){
   var nombre:String
   var edad:Int 
   init{
       this.nombre=nombre
       this.edad=edad
       if(this.edad<0) this.edad=0
   } 
    
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",-15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

Puedes comprobar modificando el ejemplo de arriba que los parámetros(variables sin indicar var/val) son realmente variables val, de forma que si queremos cambiar su valor dentro de la clase no es posible.

Otra posiblilidad sintáctica consiste en inicializar las propiedades con los parámetros justo en el momento de declarlos, en este caso no se puede usar this, al ir el nombre de la propiedad detrás de var no hay ambigüedad en el contexto de que es propiedad y que es parámetro.

class Persona (nombre:String,edad:Int){
   var nombre:String=nombre//no hay ambigüedad, no hace falta this (ni se puede indicar)
   var edad:Int=edad//no hay ambigüedad, no hace falta this
   init{
      if(this.edad<0) this.edad=0
   } 
    
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",-15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

Constructores secundarios

Recuerda que el constructor primario se declaraba en la cabecera de la clase con la palabra reservada constructor(opcional en ciertas situaciones). Un constructor secundario también se declara con la palabra reservada constructor pero se hace dentro del cuerpo de la clase.

En el siguiente ejemplo no hay constructor primario pero hay un constructor secundario.

class Persona{
   var nombre:String
   var edad:Int 
   
   constructor(nombre:String, edad:Int){
       this.nombre=nombre
       this.edad=edad
   } 
   
  
    
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15) 
p1.printMe() 
val p2 = Persona("tu",35) 
p2.printMe() 

Puede haber varios constructores secundarios

es un efecto similar a la sobrecarga de funciones

class Persona{
   var nombre="sin nombre"
   var edad:Int 
   
   constructor(edad:Int){
       this.edad=edad
   } 
   constructor(nombre:String, edad:Int){
       this.nombre=nombre
       this.edad=edad
   } 
   
  
    
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15) 
p1.printMe() 
val p2 = Persona(35) 
p2.printMe() 

puedem coexistir simultáneamente constructor primario con secundarios

En este caso es obligatorio usar la expresión this en la cabecera del secundario para delegarle los parámetros que requiera.

class Persona(var nombre:String){
   var edad:Int
   init{
       edad=99
   }
   
   constructor(nombre:String, edad:Int):this(nombre) {
       
       this.edad=edad
   } 
   
  
    
   fun printMe() {
      println(nombre+ " " + edad)
   }
} 
val p1 = Persona("yo",15) 
p1.printMe() 
val p2 = Persona("tu") 
p2.printMe() 
Última actualización: 23.09.2025

Propiedades

PROPIEDADES EN KOTLN

¿Qué es una propiedad en kotlin?

En la POO tradicional el término propiedad se refiere a lo que conocemos por “atributo” , “field” o “campo”. En kotlin es un concepto un poco más amplio pues engloba: un campo, su función accesor get y su función mutador set. Los conceptos de get y set son idénticos en Kotlin que en c++ o Java, en kotlin simplemente se hace más automática sintácticamente la relación entre un campo y sus típicos get/set

accesores: getters y setters

Vimos en ejemplos previos que para declarar una propiedad simplemente usábamos var/val como para las variables locales de las funciones. Si una variable con var/val se define en el cuerpo de la clase fuera de toda función o en el constructor primario ya se constituye en propiedad de la clase. Pero realmente una propiedad kotlin es algo más. Esta es la sintáxis completa de declaración de una propiedad:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

Fíjate que en la sintaxis anterior, que en la definición de la propiedad si queremos podemos incluir un get/set asociado a la propiedad de forma muy concisa y compacta Veamos un ejemplo que incluye los get y set

class Persona {
    var nombre:String ="chuly"

        // getter
        get() =field +  " Soy get()"

        // setter
        set(value) {
            field = value + " metido por set"
        }
}
fun main(){
    val  p=  Persona()
    println(p.nombre)
    p.nombre="rosky"
    println(p.nombre)
}

la keyword field y el concepto de back field en kotlin

Simplificadamente una propiedad podemos resumirla con la siguiente fórmula

 *propiedad = valor + set +get*. 

Así que en kotlin una propiedad es algo más que un valor. Internamente, de alguna manera se tiene que almacenar este valor y esto se hace a través una variable interna que se llama back field, Al escribir los métodos get/set a menudo querremos acceder a este valor y esto se hace con la palabra reservada field. Es decir, al back field se accede con la palabra reservada field. Lógicamente esta palabra sólo tiene sentido dentro de la declaración de una propiedad.

Si en el ejemplo anterio en lugar de field hubieramos usado el nombre de la propiedad se habría producido un efecto recursivo indeseado.

Accesores Por Defecto

Si al declarar una propiedad no especificamos accesores, kotlin crea unos por defecto. Por ejemplo:

    class Persona{
        var nombre = "chosky"
    }

Equivale a definir:

    class Persona{
        var nombre = "chosky"
            get() = field
            set(value) {
                field = value
            }
    }

A menudo los accesores por defecto son más que suficientes y no necesitamos por lo tanto escribirlos salvo que precisemos personalizarlos.

relación entre var/val y accesores

Recuerda que val genera variables no modificables, por tanto, cuando declaras una propiedad con val, solo vamos a poder personalizar el get ya que el set no es accesible. Por tanto:

val => campo + get

var => campo + get + set

Visibilidad de accesores

Más adelante estudiaremos los modificadores de visibilidad con más detenimiento. Por el momento observamos que uno de esos modificadores es private. Se puede aplicar private a un set para prohibir su uso fuera de la clase.

class Persona{

    var nombre = "chosky"
        private set
}
fun main(){
    val p=Persona()
    println(p.nombre)
    p.nombre="Rusky" //error set es private
}

En el siguiente ejemplo vemos un caso de porqué puede resultar interesante hacer private el set de una propiedad.

Tenemos una clase que encapsula unas coordenadas x, y. No queremos definir como val las propiedades porque queremos poder cambiar su valor, pero por otro lado, no queremos poder cambiar directamente las coordenadas en una instrucción de asignación ya que queremos forzar a usar las funciones miembro de la clase para cambiar las coordenadas.

class Coordenadas {
    var x: Int = 0
        private set
    var y: Int = 0
        private set

    fun moveLeft() {
        x -= if (x == 0) 0 else 1
    }

    fun moveRight() {
        x += if (x == 300) 0 else 1
    }

    fun moveUp() {
        y -= if (y == 0) 0 else 1
    }

    fun moveDown() {
        y += if (y == 300) 0 else 1
    }
}
fun main(){
    val c= Coordenadas()
    c.moveLeft()
    //c.x=77//error
}

Ten claro que main() no puede usar el set() de por ejemplo x por ser private, pero que por ejemplo, moveLeft() si puede acceder al set() de x aun siendo private ya que moveLeft() es una función miembro de la clase Coordenadas.

campos calculados.

En lenguajes como java o c++ no solía quererse definir un atributo que su valor dependía de otros atributos. Por ejemplo el área de un rectangulo depende de el ancho y alto, si cambia el ancho y el alto cambia el área, por esta razón en estos lenguajes se prefiere usar un método/funcion area() que calcule el valor del área para evitar almacenar el valor del área, simplemente, cada vez que se requiera se calcula invocando al método/función.

En kotlin en cambio si que tiene sentido definir una propiedad área quer realmente encapsula un método que hace el cálculo pero el resultado final es que obtenemos un objeto con una riqueza semántica al objeto que hace que se asemaje más a los objetos de la realidad, ya que, efectivamente en la realidad nos gusta ver el area como una propiedad de un rectángulo, no sólo como un cálculo.

class Rectangle(val width: Int, val height: Int) {
    val area: Int // property type is optional since it can be inferred from the getter's return type
        get() = this.width * this.height
}
fun main(){
    val mirectangulo= Rectangle(2,3)
    print(mirectangulo.area)
}

Backing properties

La keyword field sólo es posible usarla dentro de los accesores get/set. Por lo tanto, ya que solo los get/set son capaces de de usar field sólo los get/set son capaces de acceder directamente al valor de una propiedad. Ni siquiera otros métodos de la propia clase pueden acceder directamente al valor, se ven obligados a acceder a través de los get/set.

Observa el siguiente ejemplo. Tienes que tener claro que imprimirNombre() no está usando el field de nombre, está usando el get() de nombre. Esto realmente ya fue discutido más arriba en este documento.

class Persona {
    var nombre ="chuly"

        // getter
        get() =field +  " Soy get()"
    fun imprimirNombre() = println(nombre)

}

fun main() {
    val persona = Persona()
    persona.imprimirNombre()
}

Usar una back property asociada a una property

Si dentro de una clase escribimos una serie de funciones miembro, es habitual querer que dichos métodos puedan acceder al valor de una propiedad “saltándose el filtro” de set/get", ya que dicho filtro está pensando a menudo para las funciones externas, pero no tienen sentido para las funciones miembro.

Para conseguir que las funciones miembros “se salten los filtros set/get”, podemos usar una un segunda propiedad de respaldo de la primera que llamamos Back property. Usaremos está back property para trabajar de forma asociada con la propiedada a la que respalda. Por convenio a una Back property debemos declararla con un nombre igual que el de la propiedad a la que queremos respaldar pero comenzando con un guión bajo. El guión bajo es una norma de estilo que advierte que no se debe acceder a esta propiedad desde fuera de la clase pero no lo evita. Si queremos que el acceso no se produzca debemos además de añadir el modificador de visibilidad private.

class Persona {
    private var _nombre="chuly" //_nombre va a funcionar como propiedad de respaldo a nombre
    var nombre:String
        // getter
        get() =_nombre +  " Soy get()"
        //setter
        set(value){
            _nombre=value.uppercase() //con set() obligamos a almacenar en mayúsculas
        }
    fun imprimirNombre() = println(_nombre) //evitamos imprimir al final "soy get()"

}
fun main(){
    val  p=  Persona()
    p.imprimirNombre()
    println(p.nombre)
    //println(p._nombre) //¡ERROR!
    p.nombre="Zurky"
    p.imprimirNombre() 
    println(p.nombre)
}

Si observas el ejemplo de arriba, cuando trabajamos con un propiedad de respaldo ¿Quién va a acceder a dicha propiedad?:

  • las funciones miembro de la clase que quieren acceder a un valor “original” sin filtros get/set
  • los set/get de la propiedad asociada para mantener la lógica de valor original y valor filtrado. En muchas situaciones al trabajar con back property los set/get usarán esta back property en lugar de field. Todo depende de la lógica deseada y recuerda que cuando “se mete la lógica por medio” suele haber muchas soluciones o enfoques equivalentes.

Pese a toda la discusión anterior al respecto de back property, en general, no necesitaremos usar back properties salvo para casos bastante poco frecuentes. Para casos normales que tienen su equivalente usando field es una complicación innecesaria.

Última actualización: 23.09.2025

Sobreescribir toString()

Sobreescribir toString()

Entenderemos con detalle que signfica override al estudiar herencia. Manejamos ahora intuitivamente este concepto para utilizar la función toString() ya desde nuestros primeros ejercicios de clases con kotlin.

toString() es una función miembro cuyo objetivo es ofrecer una representación textual del estado de un objeto. Si sobreescribimos toString() podemos personalizar dicha representación textual. Debemos escribir la función toString() con el modificador override para tener una representación textual del objeto a nuestro gusto.

class Rectangle(val width: Int, val height: Int) {
    val area: Int // property type is optional since it can be inferred from the getter's return type
        get() = this.width * this.height
    
    override fun toString():String{
        return "ancho: $width alto: $height area: $area"
    }
}
val mirectangulo= Rectangle(2,3)
println(mirectangulo.area)
println(mirectangulo)
Última actualización: 23.09.2025

modificadores de visibilidad

Los modificadores de visibilidad se utilizan para controlar la visibilidad de una clase, sus miembros (propiedades, funciones y clases anidadas) y sus constructores.

Hay cuatro modificadores de visibilidad en Kotlin: private, protected, internaly public. La visibilidad predeterminada es public.

##Modificador public A diferencia de Java, en Kotlin no hay necesidad de declarar nada como public – es el modificador predeterminado. En el siguiente ejemplo A1,X1 son public igual que A2 Y x2

// by default public
class A1 {
    var x1= 10
}
public class A2{
    public var x2= 20
}
// specified with public modifier
fun main() {
    val a1 = A1()
    println(a1.x1)
    val a2 = A2()
    println(a2.x2)
}

##Modificador private Funciona de manera similar a como lo hace en Java. Cuando se utiliza private para modificar un miembro de una clase (un campo, método o clase anidada), ese miembro solo es accesible dentro de la misma clase. No se puede acceder desde fuera de la clase, ni siquiera desde las subclases.

class MyClass {
    private val myPrivateField = "Soy un campo privado"

    fun myPublicFunction() {
        println(myPrivateField) // Acceso permitido: dentro de la misma clase
    }
}

fun main() {
    val myObject = MyClass()
    myObject.myPublicFunction() // Imprime "Soy un campo privado"
    // println(myObject.myPrivateField) // Error: no se puede acceder a un miembro privado desde fuera de la clase
}

##Modificador internal Es un modificador de acceso específico de Kotlin que no existe en Java. El modificador internal se utiliza para restringir la visibilidad de un miembro de una clase a solo dentro del mismo módulo. Un módulo es un conjunto de archivos de código fuente de Kotlin compilados juntos. Por ejemplo, los ficheros de un proyecto IntelliJ se compilan juntos y por tanto son un módulo.

como organizar el código en módulos Para organizar el código en módulos separados habrá que configurar el sistema de construcción apropiadamente (grandle, maven, …). Si estás utilizando el compilador de línea de comandos de Kotlin, cada invocación del compilador se trata como un módulo separado. Esto significa que si compilas varios archivos de código fuente juntos en una sola invocación del compilador, se tratarán como un solo módulo y los miembros marcados como internal serán accesibles entre ellos.

Supongamos que A1 está escrita en el fichero A1.kt

internal class A1 {
    var x1= 10
}

y que A2 está escrita en el fichero A2.kt

class A2{
    fun miFun(){
        var x=A1()
    }
}

Si ambas clases pertenecen al mismo módulo A2 no genera error de compilación ya que tiene acceso a A1. Si A2 perteneciera a otro módulo y quisiera acceder a A1, A1 tendría que ser public

¿Existe el modo acceso paquete de java en kotlin?

En Kotlin no hay un modificador de acceso específico para el nivel de paquete como en Java. En Java, si no se especifica un modificador de acceso para un miembro de una clase, ese miembro es accesible dentro del mismo paquete. Esto se conoce como acceso a nivel de paquete. En Kotlin, si no se especifica un modificador de acceso para un miembro de una clase, ese miembro es public por defecto y es accesible desde cualquier lugar.

Aunque Kotlin no tiene un modificador de acceso específico para el nivel de paquete, puedes lograr una funcionalidad similar utilizando el modificador internal y organizando tu código en módulos separados.

El modo protected

Cuando se utiliza protected para modificar un miembro de una clase (un campo, método o clase anidada), ese miembro solo es accesible dentro de la misma clase y sus subclases.

open class MyBaseClass {
    protected val myProtectedField = "Soy un campo protegido"

    fun myPublicFunction() {
        println(myProtectedField) // Acceso permitido: dentro de la misma clase
    }
}

class MyDerivedClass : MyBaseClass() {
    fun myDerivedFunction() {
        println(myProtectedField) // Acceso permitido: dentro de una subclase
    }
}

fun main() {
    val myObject = MyDerivedClass()
    myObject.myPublicFunction() // Imprime "Soy un campo protegido"
    myObject.myDerivedFunction() // Imprime "Soy un campo protegido"
    // println(myObject.myProtectedField) // Error: no se puede acceder a un miembro protegido desde fuera de la clase o sus subclases
}

Es muy parecido a como funciona en Java pero no igual ya que en Java el modo protected también cubre los casos de modo de acceso paquete. Entonces el ejemplo anterior no es equivalente en Java ya que:

  • por defecto en java el mode de acceso es paquete pero en java public
  • no existe modo de acceso paquete en kotlin.

visibilidad de constructores

Si deseas especificar la visibilidad de un constructor en Kotlin, debes usar la palabra clave constructor y colocar el modificador de acceso antes de ella.

class MyClass private constructor(val myField: String) {
    // ...
}

Si no especificas un modificador de acceso para el constructor, será public por defecto y puedes omitir la palabra clave constructor

class MyClass1 public  constructor(val myField: String) {
    // ...
}
class MyClass2  constructor(val myField: String) {
    // ...
}
class MyClass3(val myField: String) {
    // ...
}

fun main() {
    val a1 = MyClass1("a1")
    println(a1.myField)
    val a2 = MyClass2("a2")
    println(a2.myField)
    val a3 = MyClass3("a3")
    println(a3.myField)
}
Última actualización: 23.09.2025

clases Enum

Vemos las posibilidades más esenciales de este tipo de clases. En la documentación oficial puedes consultar con más profundidad su uso. Un enum en Kotlin es un tipo especial que representa un conjunto finito de valores predefinidos. Cada valor de un enum se representa como una instancia de una clase enum, que se define utilizando la palabra clave enum class.

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

fun main() {
    val direction = Direction.NORTH

    when (direction) {
        Direction.NORTH -> println("Going North")
        Direction.SOUTH -> println("Going South")
        Direction.EAST -> println("Going East")
        Direction.WEST -> println("Going West")
    }
}

Cada valor de un enum tiene propiedades predefinidas como name, que devuelve el nombre del valor como una cadena, y ordinal, que devuelve la posición del valor en la declaración del enum

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

fun main() {
    val color = Color.BLUE

    println(color.name) // Prints "BLUE"
    println(color.ordinal) // Prints "2"
    println(color.rgb) // Prints "255"
}
Última actualización: 23.09.2025

Composición

Los dos grandes mecanismos de la POO que permiten reutilizar código son la composición y la herencia.

La composición en Kotlin es similar a la composición en Java. La composición es un principio de diseño en el que una clase tiene una relación “tiene-un” con otra clase. Esto se logra al tener una instancia de una clase como un campo en otra clase. en el siguiente ejemplo la clase Car tiene una propiedad de tipo Engine.

class Engine {
    fun start() {
        println("El motor está arrancando")
    }
}

class Car(val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main() {
    val engine = Engine()
    val car = Car(engine)
    car.start() 
}
Última actualización: 23.09.2025

Herencia

¿Qué es herencia?

la herencia en programación orientada a objetos es uno de los mecanismos para compartir y reutilizar código.

El funcionamiento básico de este mecanismo consiste en que una clase hija puede heredar todos los miembros de una clase padre sin tener que volver a implementarlos. La clase hija puede agregar nuevos miembros o modificar los existentes, además de poder implementar su propia lógica adicional. Este relación de herencia entre dos clases tiene sentido en un contexto en el que el hijo es una especialización del padre, en caso contrario es preferible usar composición como mécanismo de reutilización de código.

Usaremos la siguiente terminología Superclase o clase base: la clase que se hereda (el padre) Subclase o clase derivada: la clase que recibe la herencia(el hijo)

un ejemplo sencillo

Supongamos que tenemos que trabajar con la clase persona y la clase Alumno. Observa como en nuestro caso el Alumno es una especialización de la clase Persona ya que un Alumno incluye todo el código de la clase Persona además del suuyo propio que en este sencillo ejemplo es la existencia de una propiedad adicional grupo

class Persona {
    var nombre: String? = null
    var edad = 0
    fun imprimirPersona() {
        println("Datos personales: $nombre, $edad")
    }
}

class Alumno {
    var nombre: String? = null
    var edad = 0
    var grupo = 0.toChar()
    fun imprimirPersona() {
        println("Datos personales: $nombre, $edad")
    }

con el mecanismo de herencia, podemos indicar que la clase Alumno herede de la clase Persona y evitar todo el código duplicado del ejemplo anterior

open class Persona {
    var nombre: String? = null
    var edad = 0
    fun imprimirPersona() {
        println("Datos personales: $nombre, $edad")
    }
}

class Alumno : Persona() {
    var grupo = 0.toChar()
}
fun main() {
        val a1 = Alumno()
        a1.nombre = "Román"
        a1.edad = 14
        a1.grupo = 'a'
        a1.imprimirPersona()
}

Sintaxis De Herencia En Kotlin

Lo más basico:

  • En la clase Base, palabra reservada open En kotlin, por defecto, una clase no es heredable, hay que especificamente que queremos permitir que la clase sea heredable con la palabra reservada open
  • En la clase derivada los “:” Para indicar cual es la superclase de una clase se añade “:” despues de su nombre y a continuación el nombre de la superclase, o mejor dicho, el constructor de la superclase.

Puedes comprobar estos detalles sintácticos en el ejemplo anterior.

Constructores y herencia

Cuando creamos un objeto derivado se crea uno base, por lo tanto el código de la clase derivada tiene que tener en cuenta que constructores tiene la clase base. Veamos algunas combinaciones.

clase base con constructor por defecto

Observa en el ejemplo que la clase base no tiene un constructor explícito definido y por tanto en la clase derivada se usa el constructor por defecto

open class Base{
    var deBase="de base"
}

class Derivada: Base() {
    var deDerivada = "de derivada"

}
fun main() {
    val d = Derivada()
    println(d.deBase)
    println(d.deDerivada)
}

si la clase Derivada usa un constructor primario tendrá que referirse igualmente la constructor base por defecto

open class Base{
    var deBase="de base"
}

class Derivada(var deDerivada:String): Base()
fun main() {
    val d = Derivada("de derivada")
    println(d.deBase)
    println(d.deDerivada)
}

si la clase base tiene un constructor primario, la derivada ya no puede usar el constructor por defecto y se ve obligada a usar dicho constructor primario

open class Base(var deBase:String)

class Derivada(var deDerivada:String): Base("de base")

fun main() {
    val d = Derivada("de derivada")
    println(d.deBase)
    println(d.deDerivada)
}

puedes pobra a usar en el ejemplo anterior Base() y observar el error

Si Derivada no tiene primario y tiene un constructor secundario necesito usar la palabra reservada super para aludir al constructor de la clase base

open class Base(var deBase: String)

class Derivada : Base {
    var deDerivada: String

    constructor(deDerivada: String) : super("de base") {
        this.deDerivada = deDerivada
    }
}

fun main() {
    val d = Derivada("de derivada")
    println(d.deBase)
    println(d.deDerivada)
}

en este último ejemplo, vuelvo a observar como desde un constructor secundario de derivada preciso usar super

open class Base(var deBase: String)

class Derivada : Base {
    var deDerivada: String

    constructor(deDerivada: String) : super("de base") {
        this.deDerivada = deDerivada
    }

    constructor(deBase: String, deDerivada: String) : super(deBase) {
        this.deDerivada = deDerivada
    }
}

fun main() {
    val d1 = Derivada("de derivada")
    println(d1.deBase)
    println(d1.deDerivada)

    val d2 = Derivada("de base", "de derivada")
    println(d2.deBase)
    println(d2.deDerivada)
}

jerarquías de clases

En Kotlin, la herencia es simple, al igual que en Java. Esto significa que una clase solo puede heredar de una única clase base. No es posible heredar directamente de múltiples clases en Kotlin. Lo que sí es posible es que un hijo sea a su vez padre de otras clases.

Una clase B hereda de una A, pero una C puede heredar de B y otra D de C …. Es decir, la relación de herencia se puede extender de forma que podemos visualizar gráficamente como un arbol jeráquico.

open class Animal(val nombre: String) {
    fun hacerSonido() {
        println("El animal hace un sonido.")
    }
}

open class Perro(nombre: String) : Animal(nombre) {
    fun ladrar() {
        println("El perro ladra.")
    }
}

class PastorAleman(nombre: String) : Perro(nombre) {
    fun proteger() {
        println("El Pastor Alemán protege su territorio.")
    }
}

fun main() {
    val pastorAleman = PastorAleman("Max")
    println(pastorAleman.nombre)
    pastorAleman.hacerSonido()
    pastorAleman.ladrar()
    pastorAleman.proteger()
}

La clase Any

En Kotlin, la clase Any es equivalente a la clase Object en Java en términos de herencia y funcionalidad básica.

La clase Any es la superclase de todas las clases en Kotlin. Todas las clases en Kotlin, de forma implícita o explícita, heredan de la clase Any

Igual que de Object java, de Any se heredan, entre otras, tres famosas propiedades toString(), hashCode() y equals()

class C1 //hereda de Any
open class C2 : Any() //hereda de Any
class C3:C2() //hereda de C2 que hereda de Any

fun main() {
    val c1 = C1()
    val c2 = C2()
    println(c1.toString())
    println(c1.hashCode())
    println(c1.equals(c2))
    val c3=C3()
    println(c3.toString())
}

Sobre escritura de funciones miembro

La sobrescritura de funciones en Kotlin permite que una clase derivada proporcione su propia implementación de una función definida en su clase base utilizando las palabras clave open y override.

open class Superclase {
    open fun funcionSobrescribible() {
        println("Esta fimción puede ser sobrescrita")
    }
}
class Subclase1 : Superclase() {

}
class Subclase2 : Superclase() {
    override fun funcionSobrescribible() {
        println("Esta es la nueva implementación de la función em Subclase2")
    }
}

fun main() {
    val subclase1 = Subclase1()
    subclase1.funcionSobrescribible()
    val subclase2 = Subclase2()
    subclase2.funcionSobrescribible()
}

Observa que la función sobreescrita oculta a la función que se sobreescribe de la superclase. Si por la razón que sea, fuera necesario usar también la versión de la superclase desde la sublcase podemos hacerlo utilizando la palabra reservada super

open class Superclase {
    open fun funcionSobrescribible() {
        println("Esta fimción puede ser sobrescrita")
    }
}

class Subclase : Superclase() {
    override fun funcionSobrescribible() {
        super.funcionSobrescribible()
        println("Esta es la nueva implementación de la función em Subclase2")
    }
}

fun main() {
    val subclase1 = Subclase()
    subclase1.funcionSobrescribible()

}
Última actualización: 23.09.2025

Clases abstractas e Interfaces

Clases abstractas

Una clase abstracta es una clase que no se puede instanciar y por lo tanto está destinada a tener subclases que la extiendan. Una clase abstracta puede contener tanto métodos abstractos (métodos sin cuerpo) como métodos concretos (métodos con cuerpo).

clasesabstractas clasesabstractas

Por lo tanto, Se utiliza una clase abstracta para proporcionar una interfaz común y una implementación para sus subclases. Cuando una subclase extiende una clase abstracta, debe proporcionar implementaciones para todos los métodos y propiedades abstractas definidos en la clase abstracta.

En Kotlin, una clase abstracta se declara usando la palabra reservada abstract delante de la clase. Una clase abstracta no puede instanciar, es decir, no podemos crear un objeto para la clase abstracta. También usamos la palabra reservada abstract para declarar propiedades y métodos abstractos. observa que una clase abstracta ya que su sentido es que tenga subclases, ya es por defecto open

//abstract class
abstract class Employee(val name: String,val experience: Int) { // Non-Abstract
    // Property
    // Abstract Property (Must be overridden by Subclasses)
    abstract var salary: Double

    // Abstract Methods (Must be implemented by Subclasses)
    abstract fun dateOfBirth(date:String)

    // Non-Abstract Method
    fun employeeDetails() {
        println("Name of the employee: $name")
        println("Experience in years: $experience")
        println("Annual Salary: $salary")
    }
}
// derived class
class Engineer(name: String,experience: Int) : Employee(name,experience) {
    override var salary = 500000.00
    override fun dateOfBirth(date:String){
        println("Date of Birth is: $date")
    }
}
fun main() {
    //val emp = Employee("Praveen",2) ERROR no se puede instanciar una clase abstracta
    val eng = Engineer("Praveen",2)
    eng.employeeDetails()
    eng.dateOfBirth("02 December 1994")
}

interfaces

Las interfaces y las clases abstractas en Kotlin son similares. La diferencia principal es que las interfaces no pueden almacenar estado, mientras que las clases abstractas sí pueden. Las interfaces pueden tener propiedades, pero estas deben ser abstractas o proporcionar implementaciones de acceso.

interface MyInterface {
    val prop: Int // propiedad abstracta
    fun foo() // función sin implementación

    fun bar() {
        print(prop)
    }
}

class MyClass : MyInterface {
    override val prop = 29
    override fun foo() {
        print("foo")
    }
}

fun main() {
    val myClass = MyClass()
    myClass.foo() // Imprime "foo"
    myClass.bar() // Imprime 29
}

En Kotlin no es necesario utilizar la palabra clave abstract para declarar una propiedad o función abstracta en una interfaz.

Todas las funciones en una interfaz que no tienen un cuerpo son automáticamente abstractas y deben ser implementadas por las clases que implementan la interfaz. En el ejemplo anterior, la función foo en la interfaz MyInterface se declara sin un cuerpo, lo que significa que es abstracta.

En el siguiente ejemplo observamos que en una interfaz de Kotlin, una propiedad puede tener un valor si se proporciona una implementación para sus accesores (get y set).

interface MyInterface {
    val prop: Int
        get() = 29 // proporciona una implementación para el accesor get

    fun foo() {
        print(prop)
    }
}

class MyClass : MyInterface

fun main() {
    val myClass = MyClass()
    myClass.foo() // Imprime 29
}

Pero entonces, ¿una interface puede tener estado?.

¡No! las interfaces en Kotlin no pueden tener estado. Aunque una propiedad en una interfaz puede tener una implementación para sus accesores (get y set), esta implementación no puede depender de un campo de respaldo (backing field) ya que las interfaces no pueden tener campos. Esto significa que el valor de una propiedad en una interfaz no puede cambiar, ya que no hay un campo para almacenar su estado.

Interfaces funcionales (SAM)

Concepto idéntico en Java pero con algunas diferencias sintácticas.

Una interfaz con un solo método abstracto se denomina interfaz funcional o interfaz de método abstracto único (SAM) . La interfaz funcional puede tener varios miembros no abstractos pero solo un miembro abstracto.

Para declarar una interfaz funcional en Kotlin se usa el modificador fun.

fun interface MyFunctionalInterface {
    fun myFunction(s: String)
}

fun main() {
    val myObject = MyFunctionalInterface { s -> println(s) }
    myObject.myFunction("Hola mundo")
}

los usaremos cuando estudiemos programación funcional.

Ejemplo de polimorfismo con clase abstracta e interface

El polimorfismo es uno de los cuatro principios fundamentales de la programación orientada a objetos (junto con la encapsulación, la herencia y el abstracción). El polimorfismo permite que objetos de diferentes clases se traten como objetos de una clase común. Esto se logra mediante el uso de clases abstractas o interfaces. Cuando utilizar un interface o una clase abstracte es una cuestión de diseño orientado a objetos que trataremos más adelante. En el siguiente ejemplo, tal y como se plantea, conseguimos con ambos el mismo efecto.

Ejemplo con clase abstracta:

abstract class Shape {
    abstract fun draw()
}

class Circle : Shape() {
    override fun draw() {
        println("Dibujando un círculo")
    }
}

class Rectangle : Shape() {
    override fun draw() {
        println("Dibujando un rectángulo")
    }
}

fun main() {
    val shapes = listOf(Circle(), Rectangle())
    for (shape in shapes) {
        shape.draw()
    }
}

Ejemplo con interfaz:

interface Shape {
    fun draw()
}

class Circle : Shape {
    override fun draw() {
        println("Dibujando un círculo")
    }
}

class Rectangle : Shape {
    override fun draw() {
        println("Dibujando un rectángulo")
    }
}

fun main() {
    val shapes = listOf(Circle(), Rectangle())
    for (shape in shapes) {
        shape.draw()
    }
}

El polimorfismo es útil porque permite escribir código más genérico y reutilizable. En lugar de escribir código específico para cada clase, puedes escribir código que funcione con objetos de una clase abstracta o interfaz común y dejar que el polimorfismo se encargue de llamar a la implementación correcta del método.

Última actualización: 23.09.2025

Data Clases

A menudo creamos clases para contener algunos datos. En tales clases, algunas funciones estándar a menudo se derivan de los datos. En Kotlin, este tipo de clase se conoce como data class y se marca como data .

data class User(val name: String, val age: Int)

El compilador genera automáticamente en base a los datos las siguientes funciones de todas las propiedades declaradas en el constructor principal:

  • equals()/ hashCode()
  • toString()de la forma"User(name=John, age=42)"
  • componentN()
  • copy()

Las funciones equals() hashCode() y toString() tienen el mismo objetivo que en Java y ya las conocemos. La función copy permite crear una copia de un objeto con algunos campos modificados. Y las funciones componentN() tienen que ver con el concepto kotlin de desestructuración que veremos más adelante.

data class User(val name: String, val age: Int)

fun main() {
    val user1 = User("Alice", 25)
    val user2 = User("Bob", 30)
    val user3 = User("Alice", 25)

    println(user1) // Imprime "User(name=Alice, age=25)"
    println(user1 == user2) // Imprime "false"
    println(user1 == user3) // Imprime "true"

    val user4 = user1.copy(age = 35)
    println(user4) // Imprime "User(name=Alice, age=35)"
}
Última actualización: 23.09.2025

La palabra reservada object. Companion object, objetos singleton y objetos expressions

La palabra clave “object” en Kotlin no tiene el mismo significado que la palabra clave “object” en Java. Aunque comparten el mismo nombre, en Kotlin “object” se utiliza para diferentes propósitos. La palabra reservada “object” java se corresponde con “any” en Kotlin como ya vimos al estudiar herencia.

En Kotlin, la palabra clave “object” se utiliza para:

  • declarar miembros estáticos
  • declarar objetos singleton
  • declarar objetos anónimos con object expressions

declarar miembros estáticos

Un miembro estático en Java es un miembro de una clase que se puede acceder sin tener que crear una instancia de esa clase. Se declaran con la palabra clave static y se acceden a través del nombre de la clase. Los datos estáticos no se almacenan dentro de cada objeto si no en una zona de memoria estática común a todos los objetos.

En Kotlin, no existe una palabra clave static como en Java para declarar miembros estáticos en una clase. En su lugar, Kotlin utiliza el modificador companion object para crear miembros estáticos.

El companion object es un objeto que se asocia con la clase y permite definir propiedades y funciones que se pueden acceder directamente a través del nombre de la clase, sin necesidad de crear una instancia de la clase.

class MiClase {
    companion object {
        val miVariable: Int = 10

        fun miFuncion() {
            println("Soy una función estática.")
        }
    }
}

fun main() {
    println(MiClase.miVariable)
    MiClase.miFuncion()
}

El companion object en Kotlin ofrece algunas ventajas en comparación con el modificador static en Java. Algunas de estas ventajas precisan de conceptos que aun no vimos para analizarlas, pero por el momento podemos quedarnos con una sencilla que es que mejora la legibilidad del código.

Kotlin, al igual que Java, utiliza una zona de memoria estática para almacenar información que es compartida por todas las instancias de una clase. Esta zona de memoria se utiliza para almacenar miembros estáticos en Java o miembros de un objeto compañero en Kotlin.

En Kotlin, cuando se declara un objeto compañero dentro de una clase, sus miembros se almacenan en la zona de memoria estática y se pueden acceder directamente desde la clase sin necesidad de crear una instancia de esa clase. Esto permite que los miembros del objeto compañero se compartan entre todas las instancias de la clase.

La gestión de la memoria estática en Kotlin es manejada por la máquina virtual de Java (JVM), ya que Kotlin es un lenguaje que se ejecuta en la JVM. La JVM es responsable de asignar y liberar la memoria estática según sea necesario.

declarar objetos singleton

A diferencia de Java, donde implementarías el patrón Singleton manualmente, en Kotlin puedes declarar un objeto como un singleton directamente utilizando la palabra clave “object”. Por ejemplo:

object MiSingleton {
   fun hacerAlgo() {
       println("Haciendo algo en el singleton")
   }
}

fun main() {
   MiSingleton.hacerAlgo()
   //val miSingleton = MiSingleton() //ERROR no se puede crear una instancia de un singleton
}

declarar objetos anónimos con object expressions

Las expresiones de objeto (object expressions) en Kotlin crean objetos de clases anónimas, es decir, clases que no están explícitamente declaradas con la declaración de clase. Estas clases son útiles para un solo uso. Puedes definirlas desde cero, heredar de clases existentes o implementar interfaces.

Ejemplo desde cero Con “desde cero” nos referimos a que no se implica la herencia ni la implementación de interfaces.

fun main() {
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
        override fun toString() = "$hello $world"
    }
    println(helloWorld)
}

El ejemplo anterior puede ser útil si necesito simplemente una instancia y no me importa en absoluto el nombre de la clase ya que tengo el objeto asociado a una variable y sólo lo usaré a través de dicha variable. Sin object expression el código equivalente sería

class HelloWorld {
    val hello = "Hello"
    val world = "World"
    override fun toString() = "$hello $world"
}
fun main() {
    val helloWorld = HelloWorld()
    println(helloWorld)
}

Ejemplo heredando de un super tipo El ejemplo anterior podríamos llamarlo “heredando desde any”. Si queremos crear un objeto de una clase anónima pero que a su vez esa clase hereda de otra clase distinta de any usamos la sintaxis de herencia

open class A(x: Int) {
    public open val y: Int = x
}

fun main() {
    val a = object : A(1){
        override val y = 15
    }
    println(a.y)
}

Similar para implementar un interface

interface B {
    fun saludo()
}

fun main() {
    val  b1 = object : B {

        override fun saludo() {
            println("Hola desde b1")
        }
    }
    b1.saludo()

}

También se podría heredar de una clase e implementar simultáneamente muchos interfaces como con las clases con nombre.

Última actualización: 23.09.2025

Excepciones en Kotlin

Prácticamente es idéntico a java pero hay una importante diferencia: En Kotlin, solo hay excepciones no verificadas(unchecked) que se lanzan durante la ejecución del programa en tiempo de ejecución.

Las excepciones verificadas (checked) (marcadas) se introdujeron en java como una característica para mejorar la calidad del códgio pero con el paso del tiempo la experiencia final de los programadores es que en realidad disminuye la productividad sin ningún aumento adicional en la calidad del código. Entre otros problemas, las excepciones verificadas producen código repetitivo y muy importante hoy en día, se hacen dificiles de usar conjuntamente con expresiones lambda.

Por esto, como en muchos otros lenguajes de programación modernos, los desarrolladores de Kotlin también decidieron no incluir excepciones verificadas como una característica del lenguaje.

A continuación vemos dos características “menores” de las excepciones Kotlin que no hay en java

try/catch es una expresión y devuelve un valor.

Como ocurría con el if, cuando nos interese podemos aprovechar el hecho de que try/catch devuelve un valor

fun divide(x: Int, y: Int): Int {
    val result = try {
        x / y
    } catch (e: ArithmeticException) {
        0
    }
    return result
}
fun main() {
    println(divide(10, 2))
    println(divide(10, 0))
}

La clase Nothing y las excepciones

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-nothing.html

Nothing es un tipo especial en Kotlin que se utiliza para representar un valor que nunca existe.

Se puede usar Nothing como el tipo de retorno de una función que nunca termina, como una función que entra en un bucle infinito:

fun infiniteLoop(): Nothing {
    while (true) {
        // ...
    }
}

Para indicar el tipo de un elemento en una lista vacía para indicar que la lista no contiene elementos:

val emptyList = listOf<Nothing>()

y lo que nos interesa ahora, si una función devuelve Nothing, es una función cuyo retorno jamás se puede alcanzar lo que es equivalente a que siempre lanza una excepción.

fun failWithException(): Nothing {
    throw Exception("Error occurred")
}

Puedes consultar la documentación oficial de excepciones. Observa que la sintáxis es igual a java salvo alguna añadido como que try es una expresión y podemos usar el valor que devuelve como ocurria con el if. https://kotlinlang.org/docs/exceptions.html

Última actualización: 23.09.2025

Genéricos

clases genéricas, parámetro de tipo y argumento de tipo

Aquí la palabra “parámetro” y “argumento” la vamos a aplicar a los tipos de las clases. No confundir con los parámetros y argumentos de una función (aunque es puedan establecer paralelismos).

Se supone que conoces los rudimentos de genéricos en Java.

Las clases en Kotlin se pueden declarar usando parámetros de tipo, al igual que en Java. El parámetro de tipo consite en una letra mayúscula como T encerrada entre <>. De T decimos que es un tipo genérico y una clase que usa un tipo genérico es una clase genérica. Una clase genérica puede tener más de un tipo genérico, es decir, usar más de una letra

class Box<T>(t: T) {
    var value = t
}

¡Las clases pueden ser genéricas pero los objetos no!

Cuando instanciamos una clase genérica se proporciona un argumento para el parámetro. El argumento será un tipo concreto.

val intBox:Box<Int> = Box<Int>(1)
val stringBox:Box<String> = Box<String>("Hello")

Pero si los parámetros se pueden deducir, por ejemplo, de los argumentos del constructor, puede omitir los argumentos de tipo, por ejemplo

val box = Box(1) // 1 has type Int, so the compiler figures out that it is Box<Int>

los wildcardas de java ? *

Al trabajar con genéricos los caracteres ? y * que se combinan con extends y super no se usan en kotlin. Se sustiuyeron por un mecanimos denominado varianza

Varianza.

Vemos sólo la idea genérica sin profundizar. Es un tema importante para los programadores que necesitan escribir clases genéricas de cierto nivel de complejidad.

La varianza se refiere a cómo los subtipos de un tipo genérico se relacionan con los subtipos de sus parámetros de tipo. Hay tres tipos de varianza: covarianza, contravarianza e invarianza.

  • Covarianza: Si A es un subtipo de B, entonces Box<A> es un subtipo de Box<B>. Esto se logra utilizando el modificador out en el parámetro de tipo de Box.

  • Contravarianza: Si A es un subtipo de B, entonces Box<B> es un subtipo de Box<A>. Esto se logra utilizando el modificador in en el parámetro de tipo de Box.

  • Invarianza: Ninguna relación entre los subtipos. Esto significa que aunque A sea un subtipo de B, no hay ninguna relación entre Box<A> y Box<B>. Esto es lo que sucede cuando no se utiliza ningún modificador en el parámetro de tipo.

Puedes consultar más sobre este tema en https://kotlinlang.org/docs/generics.html#type-projections

Última actualización: 23.09.2025

cláusula typealiases

Un typealias en Kotlin es una forma de proporcionar un nombre alternativo para un tipo existente. Al usar genéricos y y en otras situaciones de manejo de tipos, se puede utilizar esta claúsula para abreviar nombres de tipos largos o para proporcionar nombres más descriptivos para tipos que pueden ser confusos.

Un typealias se declara utilizando la palabra clave typealias, seguida del nuevo nombre para el tipo y el tipo existente al que se refiere. En el siguiente ejemplo se ilustra el funcionamiento sintáctico de typealiases.

typealias StringList = List<String>

fun printAll(strings: StringList) {
    for (string in strings) {
        println(string)
    }
}

fun main() {
    val names: StringList = listOf("Alice", "Bob", "Charlie")
    printAll(names)
}
Última actualización: 23.09.2025

Delegación con cláusula by

Para entender bien el objeto de la cláusula by es necesario entender el patrón Delegation y su relación con la herencia multiple a su vez relacionado con el importante tema tan discutido “herencia vs composición”. Todas estas cuestiones se tratan en boletines a parte.

La cláusula by

En el siguiente ejemplo una clase Derived puede implementar una interfaz Base delegando todos sus miembros públicos a un objeto específico. La cláusula by provocara que se almacenará internamente en un objetos de Deriveda un objeto b y se generarán todos los métodos de Base necesarios para reenvío a b.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

El código equivalente sin cláusula by sería el siguiente

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base {
    private val base: Base = b

    override fun print() {
        base.print()
    }
}

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}
Última actualización: 23.09.2025

Clases anidadas e internas

Clases anidadas

Una clase puede estar anidada (nested) dentro de otra. La clase anidada pasa a ser un miembro de dicha clase. Por lo tanto los miembros de una clase pueden ser propiedades, funciones y ¡otras clases!.

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

fun main() {
    val demo = Outer.Nested().foo() // == 2
    println(demo)
}

en kotlin hay más combinaciones de posibilidades de anidamiento que en java ya que se pueden anidar interfaces en clases y viceversa

interface OuterInterface {
    class InnerClass
    interface InnerInterface
}

class OuterClass {
    class InnerClass
    interface InnerInterface
}

Clases internas (inner)

no es más que una clase anidada marcada con inner . Esta marca permite que la clase anidada acceda a los miembros de la clase exterior. En java el acceso a la clase exterior era inmediato, en kotlin necesitamos especificar este deseado con inner.

En el siguiente ejemplo hay error de compilación ya que foo() no puede acceder a bar

class Outer {
    val bar: Int = 1
    class Inner {
        fun foo() = bar
    }
}

fun main() {
    val demo = Outer().Inner().foo() // == 1
    println(demo)
}

si añadimos inner a la clase Inner podemos acceder a bar, incluso aunque bar sea private

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

fun main() {
    val demo = Outer().Inner().foo() // == 1
    println(demo)
}

clases internas anónimas (Anonymous inner classes)

También se les llama simplemente anónimas. Son las más usadas e importantes de las clases anidadas. Se declaran usando la sintáxis de object expressions que ya vimos con anterioridad. En el siguiente ejemplo anidamos una object expressión dentro de la clase MyActivity, pero recuerda que se pueden usar las object expresión en cualquier parte, por ejemplo directamente asociadas a una variable de la función main.

interface Greeter {
    fun greet(name: String)
}

class Person(val name: String) {
    var greeter: Greeter? = null

    fun greet() {
        greeter?.greet(name)
    }
}

class MyActivity {
    val person = Person("Alice")

    init {
        person.greeter = object : Greeter {
            override fun greet(name: String) {
                println("Hello, $name!")
            }
        }
    }
}

fun main() {
    val activity = MyActivity()
    activity.person.greet()
}
Última actualización: 23.09.2025

Programación funcional

Breve introducción a la programación funcional utilizando el lenguaje Kotlin.

Última actualización: 23.09.2025

Subsecciones de Programación funcional

sintáxis avanzada de las funciones kotlin

PROGRAMACION FUNCIONAL 1: MÁS DE SINTAXIS DE FUNCIONES

La programación funcional se basa esencialmente en el uso de funciones por lo que es conveniente observar sintaxis adicionales relativas a funciones que no vimos cuando estudiamos el concepto de función en la introducción a la programación estructurada.

Argumentos con nombre

al invocar a una función podemos indicar el nombre del parámetro al que queremos asociar el argumento. Esto es útil cuando la función tiene muchos argumentos para tener clara la correspondencia. Está técnica incluso nos permite cambiar el orden or defecto de los argumentos.

fun sumar(a: Int, b: Int): Int {
    return a + b
}
val resultado1 = sumar(2, 3)
print(resultado1)
val resultado2 = sumar(b = 3, a = 2) // usando nombres puedo cambiar orden
print(resultado2)
55

Numero variable de argumentos con varargs

Si marcamos un parámetro con varargs quiere decir que dicho parámetro puede recibir un número variable de argumentos

fun suma(vararg numeros: Int): Int {
    var resultado = 0
    for (numero in numeros) {
        resultado += numero
    }
    return resultado
}

println(suma(1, 2, 3, 4))  
println(suma(5, 10, 15))   
10
30

Realmente lo que se hace internamente es generar un array, en el caso anterior un array de enteros. ¿Cual es entonces la diferencia con declarar un array como parámetro?, pues que con esta sintáxis el argumento puede ser un array pero también una lista de los valores separados por comas con los que automáticamente se genera un array.

Funciones genéricas

Se puede escribir una función utilizando un tipo o varios genéricos. Este tipo genérico se puede usar para escribir el resto de la función tanto en los parámetros como en el cuerpo.

Los tipos genéricos se indica despues de la palabra fun indicando una letra para representar el tipo genérico entre <>

fun <T> imprimirValor(valor: T) {
    println("El valor es: $valor")
}
//observa como puedo invocar a la función con argumentos de distinto tipo
imprimirValor(42)      
imprimirValor("hola")  
imprimirValor(true)    
El valor es: 42
El valor es: hola
El valor es: true

Funciones infix

Una función infix es una función que se puede llamar utilizando la sintaxis de operador infijo en lugar de la sintaxis típica de llamada de función. Una función infix sólo puede tener un parámetro

class Persona(val nombre: String) {
    infix fun esTocayoDe(persona:Persona): Boolean {
        return this.nombre == persona.nombre
    }
}
val x = Persona("Juan")
val y = Persona("Pedro")
println(x.esTocayoDe(y))
println(x esTocayoDe y) //uso innix
false
false

Alcance de las funciones

Igual que en un lenguaje tradicional hablamos del alcance “scope” de una variable, también las funciones tienen un alcance.

Si clasificamos las funciones por alcance tenemos los siguientes tipos de funciones.

  • funciones top-level. Si lo deseamos en kotlin podemos declarar una función el nivel superior de archivo, lo que significa que no se necesita crear una clase para contener una función como en java o c#
  • funciones locales. Una función puede definirse dentro de una función y pasa a ser una función interna que llamamos local
  • funciones miembro. El alcance de la función está ligado a un objeto, similar a un método java.
  • funciones de extensión. El alcance es similar a las funciones miembro. Estuvimos utilizando constantemente funciones top-level y también escribimos funciones miembreo al ver contenidos de programación orientada objetos en Kotlin. Veremos a continuación ejemplos de funciones locales y de extensión.

Funciones locales

Una función local f es una función que se define dentro de otra función **g **. La funcion local f solo puede ser accedida por las instrucciones de g, no fuera de g. Son útiles cuando al escribir una función observamos que duplicamos código o simplemente para tener el código un poco más organizado.

el siguiente ejemplo es inútil pero ejemplificador de la sintaxis. La función sumar() es una función local ya que está definida dentro de otra llamada calcularSuma(). Desde fuera de calcularSuma la función sumar() no es accesible ya que es algo “local” a calcularSuma().

fun calcularSuma(a: Int, b: Int): Int {
    fun sumar(x: Int, y: Int): Int {
        return x + y
    }

    val resultado = sumar(a, b)
    return resultado
}
print(calcularSuma(3,4))
//print(sumar(3,2))
7

Extensión de funciones

Las extensiones de función permiten extender la funcionalidad de clases existentes añadiendo funciones pero sin modificar su código fuente original añadiendo funciones. Esto puede ser especialmente útil cuando se trabaja con bibliotecas y frameworks que no podemos o queremos modificar.
En el siguiente ejemplo añadimos a la clase standar Int la función duplicar(), observa que podemos ver la extensión de funciones como una suerte de “extend express” a la clase Int

fun Int.duplicar(): Int {
    return this*2
}

val x = 5
print(x.duplicar())
10

En el siguiente ejemplo creamos una función de extensión para Int que además puede llamarse en modo infix

infix fun Int.sumAndMultiply(other: Int): Int {
    return (this + other) * other
}

print(3 sumAndMultiply 4) //(3+4)*4
28

Sobrecarga de operadores

La sobrecarga de operadores en Kotlin se refiere a la capacidad de definir cómo se comportará un operador determinado en una clase personalizada. En otras palabras, permite que los operadores estándar, como +, -, *, /, etc., se utilicen con objetos de una clase personalizada.

Por ejemplo, queremos sumar objetos personas, para ello en la clase Persona debemos sobreescribir la función plus(). La función plus() está asociada por definición del lenguaje al operador + de tal forma que el código que escribamos dentro de la función plus de la clase Persona, es el código que se va a ejecutar si relacionamos dos personas con el operador +. Puedes consultar en kotlin.org todos los operadores que se pueden sobrecargar y sus funciones asociadas.

class Persona(val nombre: String, val edad: Int) {
    operator fun plus(other: Persona): Persona {
        val nuevoNombre = "${this.nombre} y ${other.nombre}"
        val nuevaEdad = this.edad + other.edad
        return Persona(nuevoNombre, nuevaEdad)
    }
}
val persona1 = Persona("Juan", 30)
val persona2 = Persona("María", 25)
val persona3 = persona1 + persona2

println(persona3.nombre) // Juan y María
println(persona3.edad) // 55
Juan y María
55
//ahora la suma de dos personas devuelve un entero que corresponde a la suma de edades de dos personas
class Persona(val nombre: String, val edad: Int) {
    operator fun plus(other: Persona): Int {
        return this.edad + other.edad
    }
}

val persona1 = Persona("Juan", 30)
val persona2 = Persona("María", 25)
val edad = persona1 + persona2
println(edad)
55

Funciones y eficiencia

Debido al uso intensivo de funciones en kotlin los modificadores inline y tailrec se utilizan para mejorar el rendimiento en determinados escenarios.

Funciones inline

Una función inline en Kotlin es una función que, en tiempo de compilación, se “copia y pega” el cuerpo de la función en cada lugar donde se la llama en lugar de generar una llamada a la función. Esto puede aumentar el rendimiento del programa al evitar el costo de la creación y el desmantelamiento de la pila de llamadas en tiempo de ejecución. Pero este efecto no es visible en el código fuente.

En el código fuente simplemente se añade el modificador inline y luego ya se encarga el compilador de hacer esa especie de “pegado de código” para eliminar las llamadas a la pila

En general, se recomienda utilizar la anotación inline solamente en aquellas funciones que son llamadas con frecuencia y que contienen una cantidad significativa de código. Si declaramos indiscriminadamente inline todas las funciones obtendríamos un código compilado más grande y se reduciría el rendimiento general del programa.

Nuestro objetivo ahora no es saber cuando compensa o no compensa declarar una función* inline*, nos basta saber como trabaja inline de forma que cuando usemos funciones de la biblioteca standard y consultemos su documentación, si éstas tienen el modificador* inline* sepamos simplemente que se refiere a una cuestión de eficiencia, es decir, que no tiene que ver con los resultados que devuelve la función y por lo tanto que podemos en principio despreocupar que sea inline o no.

inline fun saludo(nombre: String): String {
    return "Hola, $nombre!"
}
val miNombre = "Juan"
val mensaje = saludo(miNombre)
println(mensaje) // imprime "Hola, Juan!"
Hola, Juan!

funciones tail recursive

Ya sabemos que las funciones se pueden llamar recursivamente, por ejemplo

fun factorial(n: Int): Int {
    return if (n == 1 || n == 0) {
        1
    } else {
        n * factorial(n - 1)
    }
}
print(factorial(4))
24

La recursividad es una de las herramientas de la programación funcional. La recursividad es muy expresiva y permite resolver cierto tipo de problemas muy complicados con facilidad. No obstante, es mucho menos eficiente que la iteración.

Kotlin permite declarar las funciones recursivas como tail recursive y esto hace que automáticamente en el código compilado se genere de una versión iterativa eficiente de nuestra versión recursiva. Es decir, esta técnica nos permite disfrutar de la expresividad de la recursividad y de la eficiencia de la iteración.

Para que una una función sea considerada como *“tail recursive” *tiene que cumplir:

  • la llamada recursiva es la última operación que se ejecuta en la función.
  • se especifica el modificador tailrec lo que avisa al compilador que haga la traducción a versión iterativa correspondiente.

El ejemplo anterior de factorial, no ese puede declarar como tailrec ya que la ultima instrucción return hay una operación aritmética, no una simple llamada recursiva. La siguiente versión de factorialsí tailrec

tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n == 0) {
        acc
    } else {
        factorial(n - 1, acc * n)
    }
}
print(factorial(4))
24

Se pierde libertad y expresividad al escribir el código, pero de esta manera se pueden evitar el bajo rendimiento y los desbordamientos de pila ya que el compilador trabaja internamente con version iterativa que el propio compilador genera automáticamente.

Muchos programadores que necesitan resolver un problema típicamente recursivio siguen este proceso:

  1. Escribe su código recursivo libremente disfrutando de la expresividad de la recursividad.
  2. Una vez alcanzada la solución y bien entendido el problema, si se preveen desbordamientos de pila o problemas de eficiencia se intenta reescribir la solución anterior como tail recursive para que el compilador genere en el código compilado al versión iterativa. No siempre es posible obtener una versión tail recursive, depende del problema a resolver.
  3. Si nuestra solución no tiene una versión equivalente escrita en formato tail recursive, entonces el programador intenta hacer él mismo una versión iterativa para lo que existen diversas técnicas algorítmicas.

Asignar funciones a variables. El operador de referencia a funciones ::

Se puede utilizar el operador “: :” para asignar una función a una variable o a una constante. Este operador se conoce como operador de referencia de función y permite crear una referencia a una función existente. Es posible asignar una función a una variable de varias formas, por ejemplo con el operador ::

fun sumar(a: Int, b: Int): Int {
    return a + b
}
fun restar(a: Int, b: Int) : Int {
    return a - b
}


var miFuncion = ::sumar
println(miFuncion(2,3))
miFuncion = ::restar
println(miFuncion(10,15))
5
-5

Si la función es miembro de una clase también se puede obtener una referencia a dicha función con la sintaxis Clase::funcion

class Persona(val nombre: String) {
    fun saludar() {
        println("Hola, soy $nombre")
    }
}
val funMiembroReferencia = Persona::saludar
val persona = Persona("Juan")
funMiembroReferencia(persona) //equivalente a persona.saludar()
Hola, soy Juan

Sintaxis para expresar tipos de función. Firma de la función

En ciertas situaciones necesitamos referirnos al tipo de una función. El tipo de una función se refiere al conjunto formado por del tipo de sus parámetros y el tipo de retorno. A este conjunto también se le conoce por el término firma de la función. La sintaxis general es:

(lista de los tipos de parámetros separados por comas) -> tipo de retorno

Ejemplos

(Int, Int) -> Int describe una función que se le pasan dos enteros y devuelve un entero

(Int) -> String función que se le pasa un entero y devuelve un String

() -> String función que no se le pasa nada y devuelve un String

(Int) -> Unit función que se le pasa un Int y no devuelve nada

Funciones anónimas

Una función anónima es una función que no tiene un nombre explícito asociado a ella.

Las funciones anónimas son útiles cuando necesitamos definir una función simple que se utilizará solo una vez en el programa y no es necesario darle un nombre explícito. También es interesante cuando queremos asociar una función a una variable.

val sum = fun(a: Int, b: Int): Int {
    return a + b
}

// Llamada a la función anónima
val result = sum(2, 3)
println("El resultado de la suma es $result")
El resultado de la suma es 5

Funciones lambda

Las funciones lambda son otra forma de escribir funciones anónimas. La diferencia principal entre una función anónima y una lambda es que la sintaxis para definir una función lambda es más corta y concisa. Observa que la sintaxis de una función lambda usa la sintaxis tipo de función que comentamos más arriba. De hecho, se puede ver a una expresión lambda como una instancia de un tipo de función.

En Kotlin, una función lambda se define utilizando la sintaxis { argumentos -> cuerpo de la función }. La función lambda también puede ser asignada a una variable o pasada como un parámetro a otra función, de manera similar a una función anónima.

En general se usan más la lambda que las funciones anónimas, no obstante, en ciertas situaciones que iran apareciendo poco a poco se podra observar que hay situaciones en las que se prefieren las funciones anónimas

val sum = { a: Int, b: Int -> a + b }  //ahora como lambda

// Llamada a la función lambda
val result = sum(2, 3)
println("El resultado de la suma es $result")
El resultado de la suma es 5

Relación entre lambda y tipo de función

La firma de la lambda { a: Int, b: Int -> a + b } en Kotlin es (Int, Int) -> Int

Todas las funciones tienen una firma y por tanto se describe su firma con un tipo de función. Por ejemplo

fun suma(a: Int, b: Int): Int { return a + b }

su firma también es (Int, Int) -> Int

Por lo tanto los tipos de función describen la firma de una función. Ocurre además que la descripción de la firma se hace con una sintáxis bastante parecida, aunque no igual, a la sintaxis lambda, pero son cosas diferentes. Con una lambda creamos una instancia de una función. La sintáxis de tipos de función aquí la emplemaos “teóricamente” para también se usa en el código en ciertas situaciones, por ejemplo, para describir el tipo de un parámetro que queremos que reciba como argumento una función. Esto lo veremos más adelante.

Funciones que aceptan otra función como parámetro

Esta es una de la situaciones que la necesitamos expresamente incluir en nuestro código la sintaxis de tipos de función.

Un parámetro no es más que un tipo especial de variable local. Como para toda variable debo indicar su tipo al definirla. Si quiero que la variable se asocie a un Int indicaré el tipo Int, pero si quiero que la variable se asocie a una función, indicare ¡el tipo de la función!

fun sumar(a: Int, b: Int): Int {
    return a + b
}

fun restar(a: Int, b: Int): Int {
    return a - b
}

fun operarDosNumeros(num1: Int, num2: Int, operacion: (Int, Int) -> Int): Int {
    return operacion(num1, num2)
}
println(operarDosNumeros(5, 3, ::sumar))
println(operarDosNumeros(5, 3, ::restar))
8
2

Sintaxis especial para invocar funciones que tienen por último parámetro una lambda

Es una sintaxis muy utilizada y merece una indicación especial.

Cuando se llama a una función, que a su vez tiene una función lambda como último argumento, se pueden escribir este último argumento fuera de los paréntesis. Es por una cuestión de legibilidad y se recomienda este formato en la guia de estilo kotlin.

fun restar(a: Int, b: Int): Int {
    return a - b
}

fun operarDosNumeros(num1: Int, num2: Int, operacion: (Int, Int) -> Int): Int {
    return operacion(num1, num2)
}
println(operarDosNumeros(5, 3, ::restar))
//ahora vamos a sumar con lambda
println(operarDosNumeros(5, 3, {x,y->x+y}))
println(operarDosNumeros(5, 3) {x,y->x+y})   //se prefiere
2
8
8

Funciones que devuelven una función

Otra situación en la que necesito la sintáxis de tipo de función.

Como sabemos cuando escribimos la definición de una funcion, tiene que concordar el tipo indicado en la cabacera con el tipo de lo que se devuelve en el return, por lo tanto:

  • en la definición de la función, en la cabecera, al indicar el tipo de retorno de la función observaremos que como queremos devolver una función el tipo de retorno se describe con un tipo de función. En el ejemplo de abajo con (Int))->Int
  • en el return devolveremos una función. En el ejemplo conseguimos devolver una función a través de una lambda que concuerda con el tipo de retorno indicado en la definición de la función.

Veamos dos ejemplos, que efectivamente no son muy útiles pero ejemplifican de forma sencilla el concepto de “devolver una función”

fun square(x: Int) = x * x

fun operation():  (Int) -> Int {
    return ::square
}

val func = operation()//operation() devolvió la función square() que ahora está enganchada a la variable func
println( func(4) )
16
fun sumar(num1: Int): (Int) -> Int {
    return { num2 -> num1 + num2 }
}

val sumarCinco = sumar(5) //podemos imagina que sumarCinco tiene dentro  la lambda {num2->5+num2}
println(sumarCinco(3))
8

La keyword it en expresiones lambda

Cuando la expresión lambda sólo tiene un parámetro la expresión lambda se puede simplificar asumiendo que dicho parámetro se llama it, lo que nos permite escribir todo más conciso

val incrementarEn1: (Int) -> Int = { x -> x + 1 }
//val incrementarEn1 = { x:Int -> x + 1 } así tb se infieren tipos
val y = incrementarEn1(5) 
println(y)
//ahora con it
val incEn1: (Int) -> Int = { it + 1 }
val x = incrementarEn1(5) 
println(x)
6
6
Última actualización: 23.09.2025

algunos conceptos básicos de programación funcional

(algunas) caracteristicas básicas del paradigma de programación funcional

Es un tema extenso. Las siguiente lista de caracterísiticas no es una lista definitiva y son simplemente un punto de partida para ir comprendiendo que es la programación funcional.

  • Las funciones son ciudadanos de primera clase: Las funciones son valores que pueden ser asignados a variables, pasados como argumentos y devueltos como resultados.
  • Inmutabilidad de los datos: Los datos no deben cambiar una vez que se han creado. En lugar de modificar los datos existentes, las funciones deben crear nuevas estructuras de datos a partir de las existentes.
  • Programación basada en expresiones: Las expresiones son evaluadas para producir valores. Las expresiones son preferibles a las sentencias, que modifican el estado.
  • Evaluación perezosa: Los valores se calculan solo cuando se necesitan. Esto puede mejorar la eficiencia de los programas al evitar el cálculo innecesario de valores que no se utilizan.
  • Declaratividad: Los programas se definen en términos de qué se debe hacer, no de cómo hacerlo.

Características más importantes que deben cumplir las funciones para trabajar con el paradigma de programación funcional

Para atender a los principios de este paradigma las funciones del lenguaje deben de:

  • ser funciones puras
  • permitir la composición de funciones
  • permitir el trabajo con recursividad
  • poder comportarse como funciones de alto orden

Funciones puras

Las funciones deben producir el mismo resultado para una entrada dada y no tener efectos secundarios. Esto hace que las funciones sean más fáciles de razonar y depurar. La siguiente funcion es pura, en el ejemplo siempre que recibe los valores 3 y 4 produce 7

fun suma(num1:Int,num2:Int) = num1+num2
println(suma(3,4))
println(suma(3,4))
println(suma(3,4))
println(suma(3,4))
7
7
7
7

esta característica es muy importante para evitar los temidos efectos colaterales del uso de variables de alcance no local a la función.

La siguiente función no es pura ya que si la invocamos con los mismos argumentos puede producir diferentes valores.

var total=0
fun suma(num1:Int,num2:Int):Int{
    total=total+num1+num2
    return total
} 
println(suma(3,3))
println(suma(3,3))
6
12

Dicho de forma más llana, las funciones son puras si trabajan siempre con variables locales. Utilizar variables NO locales genera aplicaciones más dificiles de depurar.

Composición de funciones

Con la composición de funciones conseguimos alcanzar una solución con el método de divide y venceras y al mismo tiempo reusar código. La composición de funciones es una técnica de programación en la que se combinan varias funciones para crear una función más compleja. En programación funcional las funciones gigantes se evitan.

fun alCuadrado(numero:Int)= numero*numero
fun sumaDeCuadrados(num1:Int,num2:Int)= alCuadrado(num1)+alCuadrado(num2)
fun cuadradoAntecesorMasSucesor(numero:Int)= sumaDeCuadrados(numero-1,numero+1)
print(cuadradoAntecesorMasSucesor(3))
20

Funciones recursivas

ya conocemos está técnica que es vital para programación funcional ya que permite resolver problemas evitando bucles y razonando a un nivel de abstracción superior, es decir, razonamientos más alejados de la arquitectura hardware.

Funciones de orden superior(high order) y funciones de primera clase (first class)

Los términos first class function (funcion de primera clase ) y high order function (orden superior) pueden ser confusos. Nosotros siguiendo a kotlin.org obtenemos las siguientes definiciones.

Para liar más a las funciones de primera clase también se les llama de primer orden, término que fácilmente se confunde con orden superior.

Un ejemplo con funciones high order

El paso de funciones por parámetro de una función y la devolución de una función ya fue estudiada anteriormente. Observamos en el siguiente ejemplo que calculateCost() y getDiscount() son high order functions

class Product(val name: String, val price: Double)

val shippingCost = 4.0
val taxRate = 0.08


//calculateCost es una high order porque su segundo parámetro es una función
fun calculateCost(product: Product, discount: (Double) -> Double): Double {
    val subtotal = discount(product.price) + shippingCost
    val tax = subtotal * taxRate
    return subtotal + tax
}

//getDiscount es una high order porque devuelve una función, una de las lambdas del when
fun getDiscount(discountCode: String): (Double) -> Double {
    return when (discountCode) {
        "10%_OFF" -> { p -> p * 0.9 }
        "5_BUCKS_OFF" -> { p -> Math.max(p - 5.0, 0.0) }
        else -> { p -> p }
    }
}


val product = Product("Widget", 10.0)

println(calculateCost(product, getDiscount("10%_OFF")))
println(calculateCost(product, getDiscount("5_BUCKS_OFF")))
14.04
9.72

Funciones de orden superior en la librería standard

Para enrqiuecer la capacidad de programación funcional en Kotlin, la librería standar tiene un buen número de funciones de orden superior.

Veamos como ejemplos Repeat() y takeif(). Recuerda que los bucles y los if de la programación imperativa no pertenecen al estilo funcional de escribir programas. repeat() será la función que permite conseguir un efecto equivalente a los bucles imperativos y takeif() al if. Hay un momento para cada cosa, hoy por hoy ambos estilon son importantes y los vamos a encontrar mezclados dentro de una aplicación si fue echa con un estilo de programación actualizado.

repeat()

Recuerda que en el notebook anterior indicamos la sintaxis especial para invocar funciones cuyo último argumento es una función lambda

Si consultas en la documentación de Kotlin la función repeat() observarás que tiene la siguiente firma

repeat(times: Int, action: (Int) -> Unit)

Fíjate mucho en el segundo parámetro action, su tipo es una “tipo función”, por lo tanto, cuando se invoque a esta función se le pasará como segundo argumento una función y por tanto repeat() es una funcion de orden superior.

El funcionamineto de repeat() es el siguiente, el primer parámetro, times, es el número de veces que se debe ejecutar la función que recibirá el segundo parámetro.

En definitva repeat() es una manera de iterar pero con un estilo más funcional, en lugar de escribir un bucle, invocamos a la función repeat().

El parámetro Int de la función te puede resultar confuso no le des importancia, simplemente es un índice de la iteración que podemos usar en el cuerpo de la actión como vemos en el último ejemplo.

repeat(3, { println("Hola") })

repeat(3) { println("Adios") } //MEJOR ASÍ con código lambda fuera de paréntesis
// greets with an index
repeat(3) { index ->
    println("Hello with index $index")
}
Hola
Hola
Hola
Adios
Adios
Adios
Hello with index 0
Hello with index 1
Hello with index 2

takeif()

si consultamos la doc de kotlin

inline fun <T> T.takeIf(predicate: (T) -> Boolean): T?

Observamos que:

En el sigueinte ejemplo observamos que takeIf devuelve el número sobre el que se aplica si la función lambda del parámetro devuelve true, en caso contrario takeif() devuelve null

// Ejemplo con takeIf() miembro de  Int
var numero = 5
var numeroConCondicion = numero.takeIf({ it < 10 })
println(numeroConCondicion)
numero=12
numeroConCondicion = numero.takeIf { it < 10 }
println(numeroConCondicion)
5
null

Se puede también aplicar también por ejemplo a Strings

val miNombre="Chuly Pachuly"
val nombreConCondicion= miNombre.takeIf { it.length<20 }
println(nombreConCondicion)
Chuly Pachuly

Realmente como takeIf() es una función genérica (observa la firma indicada más arriba), se puede aplicar a cualquier tipo, por ejemplo a un tipo personalizado Persona.

Observa que en el siguiente ejemplo , se utiliza el operador de navegación segura (?.) para acceder al nombre de esMayorDeEdad1. Esto se debe a que esMayorDeEdad1 es potencialmente nulo ya que takeif puede devolver null. Si la persona a la que hace referencia esMayorDeEdad1 no es mayor de edad (es decir, si edad es menor que 18), entonces esMayorDeEdad1 será null. En ese caso, si intentamos acceder al nombre directamente, nos encontraríamos con un error de NullPointerException.

Para evitar este error, utilizamos el operador de navegación segura (?.) en la línea println(esMayorDeEdad1?.nombre). Esto significa que si esMayorDeEdad1 es null, la expresión se evaluará como null en lugar de lanzar una excepción.

En la línea println(esMayorDeEdad2), no necesitamos utilizar el operador de navegación segura porque no estamos intentando acceder a un miembro de objeto en esMayorDeEdad2. En su lugar, simplemente imprimimos el valor de esMayorDeEdad2, que puede ser nulo si la persona no es mayor de edad.

class Persona(val nombre: String, val edad: Int)

val persona1 = Persona("Juan", 20)
val persona2 = Persona("Ana", 16)

val esMayorDeEdad1 = persona1.takeIf { it.edad >= 18 }
val esMayorDeEdad2 = persona2.takeIf { it.edad >= 18 }

println(esMayorDeEdad1?.nombre)//necesitamos el operador ? por si contiene null
println(esMayorDeEdad2)
Juan
null

Un ejemplo combinado repeat() y takeif()

El ejemplo utiliza el operador elvis ?: de tal forma que cuando takeIf devuelve el valor null se sustituye por la cadena “No es par”

repeat(5) {
        println((1..10).random().takeIf { it % 2 == 0 } ?: "No es par")
}
No es par
4
No es par
4
No es par

Funciones de orden superior para trabajar con colecciones y sequence

Manipular los datos de colecciones y sequence es un contexto que se adapta muy bien para el estilo de programación funcional. Lo vemos un notebook posterior.

funciones de orden superior y colecciones

FUNCIONES DE ORDEN SUPERIOR PARA TRABAJAR CON COLECCIONES

Simplemente pretendemos ver algunas posibilidades para intuir como procesar una colección con la ayuda de las funciones de orden superior. Podremos usar un gran número de funciones de orden superior de la librería standar o escribir otras nuevas personalizadas a nuestras necesidades. En este documento nos limitamos a ver ejemplos con algunas de las funciones de orden superior más comunes de la librería standard y manejadas de forma sencilla e intuitiva ya que su uso en profundidad requiere más dedicación que la que aquí se le presta.

Ya debió quedar claro de los documentos anteriores que todas estas funciones por ser de orden superior aceptaran una función como parámetro y/o devolveran una función como resultado.

Observaras que muchas de estas funciones iteran automáticamente sobre la colección elemento a elemento lo que no spermite eliminar la setencia for o while del código para conseguir así un estilo más funcional.

map

Toma una colección y una función de transformación como parámetros, y devuelve una nueva colección que resulta de aplicar la función a cada elemento de la colección original.

val numeros = listOf(1, 2, 3, 4, 5)
val numerosCuadrados = numeros.map { it * it }
println(numerosCuadrados) 
[1, 4, 9, 16, 25]

filter

Toma una colección y una función de predicado como parámetros, y devuelve una nueva colección que contiene únicamente los elementos de la colección original que cumplen con el predicado.

val numeros = listOf(1, 2, 3, 4, 5)
val numerosPares = numeros.filter { it % 2 == 0 }
println(numerosPares) 
[2, 4]

fold

Toma una colección, un valor inicial y una función de acumulación como parámetros, y devuelve el resultado de aplicar la función de acumulación a cada elemento de la colección, empezando por el valor inicial.

val numeros = listOf(1, 2, 3, 4, 5)
val suma = numeros.fold(0) { acumulado, elemento -> acumulado + elemento }
println(suma) 
15
val words = listOf("Hola", " ", "Mundo", "!")

val concatenated = words.fold("") { acc, word ->
    acc + word
}

println(concatenated) 
Hola Mundo!

foreach

Se ejecuta la acción indicada por la lambda para cada elemento de la colección

val numeros = listOf(1, 2, 3, 4, 5)

numeros.forEach { numero ->
    println(numero)
}
1
2
3
4
5

takewhile

si foreach lo puedes ver como una suerte for imperativo automático que recorre toda la colección, takewhile lo podemos ver como un while automático que recorre la colección hasta que deja de cumplirse una condición

val numeros = listOf(1, 2, 3, 4, 5)

val numerosTomados = numeros.takeWhile { it < 4 }

println(numerosTomados) 
[1, 2, 3]

groupBy

Toma una colección y una función de transformación como parámetros, y devuelve un mapa que agrupa los elementos de la colección por el resultado de aplicar la función de transformación a cada elemento.

data class Persona(var nombre:String,var edad:Int) //recuerda que data class ya provee de toString()
val personas = listOf(
    Persona("Juan", 25),
    Persona("María", 30),
    Persona("Pedro", 25),
    Persona("Lucía", 30)
)
val personasPorEdad = personas.groupBy { it.edad }
println(personasPorEdad) 
{25=[Persona(nombre=Juan, edad=25), Persona(nombre=Pedro, edad=25)], 30=[Persona(nombre=María, edad=30), Persona(nombre=Lucía, edad=30)]}

Ordenar

Es un tema extenso y tiene muchas posibilidades. Como intuimos habrá funciones de alto orden relacionadas con sorting.

Nos restringimos aquí a ver un ejemplo escrito de una manera característica del estilo kotlin. En el ejemplo observamos:

class FullName(val name: String, val surname: String) {
    override fun toString(): String = "$name $surname"
}

val names = listOf(
        FullName("B", "B"),
        FullName("B", "A"),
        FullName("A", "A"),
        FullName("A", "B"),
)

println(names.sortedBy { it.name })
// [A A, A B, B B, B A]
println(names.sortedBy { it.surname })
// [B A, A A, B B, A B]
println(names.sortedWith(compareBy(
    { it.surname },
    { it.name }
)))
// [A A, B A, A B, B B]
println(names.sortedWith(compareBy(
    { it.name },
    { it.surname }
)))
// [A A, A B, B A, B B]
[A A, A B, B B, B A]
[B A, A A, B B, A B]
[A A, B A, A B, B B]
[A A, A B, B A, B B]

funciones de orden superior y sequence

FUNCIONES DE ORDEN SUPERIOR Y SEQUENCE

que es sequence y en que se diferencia de las colecciones

Una sequence es una secuencia de elementos similar en muchos aspectos a una lista o a un array. Así por ejemplo una lista Kotlin y una sequence tienen muchos métodos en común como : map, filter, reduce, take, drop, flatten, count, distinct, entre otras.

También hya diferencias y la difencia más importante, por sus consecuencias, es que los elementos de una secuencia pueden ser procesados de manera perezosa (lazy), a medida que se van necesitando, de tal forma que en una Sequence no se calculan todos los elementos de antemano, sino que se calculan bajo demanda, en el momento justo en que se necesitan. Esto puede tener una gran ventaja en términos de eficiencia y uso de recursos cuando se trabaja con secuencias grandes o complejas.

Hay muchas formas de obtener una Sequence. Una sencilla es a partir de una colección ya existente y esto se puede hacer de muchas formas, como por ejemplo con la función asSequence()

Una vez que tenemos una Sequence, podemos aplicar una serie de operaciones para procesar los elementos de manera perezosa, de forma similar a lo que podemos hacer con las colecciones “tradicionales” pero con otro rendimiento y otras posibilidades de computación.

    val lista = listOf(1, 2, 3, 4, 5)

    val secuencia = lista.asSequence()
        .filter { it % 2 == 0 } // Filtramos los números pares
        .map { it * 2 } // Duplicamos cada número
        .sortedDescending() // Ordenamos en orden descendente

    println("Con sequence")
    secuencia.forEach { println(it) }
    //desde punto de vista de operaciones idem si lo hacemos directamente con la lista

    val listaProcesada= lista.filter { it % 2 == 0 }
        .map { it * 2 }
        .sortedDescending()

    println("Directamente con lista")
    listaProcesada.forEach { println(it) }
Con sequence
8
4
Directamente con lista
8
4

UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML

UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML

En esta unidad estudiaremos:

Gestión de información almacenada en ficheros, flujos, haciendo especial hincapié en los formatos JSON y algo de XML mediante aplicaciones informáticas escritas en Java.

a) Gestión de flujos, ficheros secuenciales, Acceso Directo y Directorios: desarrollo de aplicaciones que gestionan información almacenada en ficheros secuenciales, de acceso directo y en el sistema de directorios. En ella se aprenderá a identificar y utilizar las clases específicas para operar con cada tipo de fichero y con el sistema de directorios y a manejar las excepciones para el tratamiento de los posibles errores.

b) Gestión de ficheros JSON y, en menor medida, XML: desarrollo de aplicaciones que gestionan información almacenada en ficheros JSON (con biblioteca Gson) y una introducción a Moshi. También veremos algo de XML, y prenderemos a utilizar los procesadores DOM y SAX, las clases específicas para el tratamiento de la información contenida en un fichero XML, las clases específicas para la vinculación de objetos, las bibliotecas para conversión de documentos XML a otros formatos y a manejar las excepciones para el tratamiento de los posibles errores.

Subsecciones de UD 1. Acceso a ficheros, flujos, serialización de objetos, ficheros JSON y XML

01.01 Java IO. Acceso a ficheros, flujos, serialización de objetos.

UD 01.01. Java IO. ficheros y flujos

En este apartado estudiaremos las principales clases y métodos de la API de Java para el acceso a ficheros y flujos de datos:

El API Java IO proporciona clases para entrada y salida a través de flujos de datos, serialización y sistemas de ficheros (leer y escribir datos en archivos, así como para leer y escribir datos en la consola).

En este apartado estudiaremos cómo se organizan los archivos y directorios en un sistema de archivos y cómo acceder a ellos con la clase java.io.File (el modo tradicional de hacerlo).

Luego veremos cómo leer y escribir datos de archivo con las clases de flujo (Streams IO, no confundir con la API Streams).

Concluimos discutiendo formas de leer la entrada del usuario en tiempo de ejecución utilizando la clase Console.

Subsecciones de 01.01 Java IO. Acceso a ficheros, flujos, serialización de objetos.

01.00 Java IO. Introducción.

1. Java I/O

Las aplicaciones Java, ¿qué pueden hacer fuera del ámbito de gestionar objetos y atributos en la memoria? ¡Al cerrar el programa se pierde todo! ¿Cómo pueden guardar datos para que la información no se pierda cada vez que el programa se termina? ¡Usar archivos, por supuesto!, es la primera opcion (o cualquier sistema de persistencia más avanzado, como bases de datos, que abordaremos en la siguiente unidad).

Se pueden realizar programas sencillos que guarden el estado actual de una aplicación en un archivo cada vez que la aplicación se cierra y luego cargue los datos cuando se ejecute la aplicación la próxima vez. De esta manera, la información se preserva entre ejecuciones del programa. Es lo que se denomina, persistencia.

Este apartado estudiaremos el API java.io para interactuar con archivos y flujos. Comenzamos describiendo cómo se organizan los archivos y directorios en un sistema de archivos y mostramos cómo acceder a ellos con la clase java.io.File (el modo tradicional de hacerlo). Luego veremos cómo leer y escribir datos de archivo con las clases de flujo (Streams IO, no confundir con la API Streams). Concluimos discutiendo formas de leer la entrada del usuario en tiempo de ejecución utilizando la clase Console.

En el siguiente apartado, dedicado a “Java NIO.2”, veremos cómo Java proporciona técnicas más poderosas (y rápidas) para gestionar archivos.

2. Archivos y directorios

Comenzamos este apartado repasando qué es un archivo y un directorio en un sistema de archivos. También presentamos la clase java.io.File y veremos cómo usarla para leer y escribir información de archivos.

2.1. Sistema de Archivos

Para empezar es necesario saber qué es un sistema de archivos. Los datos se almacenan en dispositivos de almacenamiento persistentes, como discos duros o tarjetas de memoria, por ejemplo.

Un archivo es un registro dentro del dispositivo de almacenamiento que contiene datos.

Los archivos se organizan en jerarquías utilizando directorios.

Un directorio es una ubicación que puede contener archivos y otros directorios.

Cuando trabajamos con directorios en Java, a menudo los tratamos como archivos. De hecho, se usan muchas de las mismas clases para operar en archivos y directorios. Por ejemplo, un archivo y un directorio pueden renombrarse con el mismo método de Java.

Para interactuar con archivos, necesitamos conectarnos al sistema de archivos. El sistema de archivos se encarga de leer y escribir datos en un ordenador. Los diferentes sistemas operativos utilizan sistemas de archivos diferentes para gestionar sus datos. Por ejemplo, los sistemas basados en Windows usan un sistema de archivos diferente que los basados en Unix (Linux, …). La JVM se conectará automáticamente al sistema de archivos local, lo que te permite realizar las mismas operaciones en múltiples plataformas.

Directorio raíz

El directorio raíz (root) es el directorio superior en el sistema de archivos, del cual heredan todos los archivos y directorios:

  • En Windows, se denota con una letra de unidad, como c:\\.
  • En Linux se denota con una barra diagonal simple, /.
Rutas

Una ruta es una representación en cadena de un archivo o directorio dentro de un sistema de archivos. Cada sistema de archivos define su propio carácter separador de rutas que se utiliza entre las entradas de directorio. El valor a la izquierda de un separador es el padre del valor a la derecha del separador. Por ejemplo, el valor de ruta /home/otto/cole.txt significa que el archivo cole.txt está dentro del directorio otto, con el directorio otto dentro del directorio home.

Las rutas pueden ser absolutas o relativas.

Ejemplo de estructura de directorios en Linux Ejemplo de estructura de directorios en Linux Rerefencia: https://docs.oracle.com/javase/tutorial/essential/io/path.html

En la figura anterior muestra un árbol de directorios de ejemplo que contiene un único nodo raíz. Microsoft Windows admite varios nodos raíz. La familia de sistemas operativos basados en Unix (Linux, Solaris, macOS, etc.) admite un único nodo raíz, que se indica mediante el carácter de barra diagonal.

Un archivo se identifica por su ruta en el sistema de ficheros, empezando por el nodo raíz. Por ejemplo, en el sistema de ficheros de Windows, la ruta C:\Programas\holamundo.kt identifica un archivo llamado holamundo.kt que se encuentra en el directorio Programas en la unidad C:.

En la figura: /home/sally/statusReport y c:\home\sally\statusReport son rutas absolutas pa SO Unix y Windows, respectivamente.

El delimitador es específico del sistema de archivos. En Linux \ y en Windows /.

2.1. Almacenar Datos como Bytes

Los datos se almacenan en un sistema de archivos (y en la memoria) como un 0 o 1, llamado bit. Dado que es realmente difícil para las personas leer/escribir datos que son sólo 0s y 1s, se agrupan en un conjunto de 8 bits, llamado byte.

¿Qué pasa con el tipo primitivo byte de Java? Como veremos en el apartado de flujos de E/S, a menudo se leen o escriben valores en flujos utilizando valores de byte y arrays de bytes, si bien los métodos recogerán valores enteros para el control de fin de flujo o lectura/escritura.

Caracteres ASCII

Usando un poco de aritmética (2^8), vemos que un byte se puede establecer en uno de 256 posibles permutaciones. Estos 256 valores forman el alfabeto básico del Sistema Informático para poder escribir caracteres como a, # y 7. Históricamente, los 256 caracteres se conocen como caracteres ASCII, basado en el estándar de codificación que los definió. Teniendo en cuenta todos los idiomas (como galego e castelán) y emojis disponibles hoy en día, 256 caracteres es realmente restrictivo. Se han desarrollado muchos estándares más nuevos que se basan en bytes adicionales para mostrar caracteres.

Última actualización: 23.09.2025

01.01 La clase File


1. Clases para trabajar con ficheros (java.io.File, RandomAccessFile, …)

Los flujos de entrada/salida (streams I/O), que veremos en esta unidad, trabajan con gran variedad de fuentes de datos, incluyendo archivos, sin embargo, los flujos no proporcionan todas las operaciones comunes a los archivos de disco.

Existen clases de E/S para trabajar con ficheros que no son orientas a flujos. Algunas de ellas son:

  • java.io.File: ayuda a escribir código independiente de plataforma para examinar y manipular archivos y directorios. Esta clase era el mecanismo utilizado para E/S de archivos en Java antes de Java 7: https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/io/File.html
  • java.io.RandomAccessFile: proporciona acceso aleatorio a archivos.
  • java.nio.file.Path: interface añadida en Java 7 y que permite una forma de trabajar con rutas de archivos y directorios más eficiente. Esta interfaz se emplea con la clase Files para proporcionar un uso más eficiente y completo para acceder a operaciones adicionales, como atributos de archivos, o excepciones de E/S que ayudan a diagnosticar problemas de E/S.
  • java.nio.file.Files: clase dispone de métodos estáticos para operaciones de archivos y directorios, así como creación de flujos de entrada/salida.

La clase File

La primera clase que estudiaremos es una de las más empleadas (y antigua) del API java.io: la clase java.io.File.

La clase File se utiliza para leer información sobre archivos y directorios existentes, listar el contenido de un directorio o crear/eliminar archivos y directorios.

Una instancia de una clase File representa la ruta a un archivo o directorio específico en el sistema de archivos, pero no contiene los datos del archivo o directorio (el archivo podría no existir).

La clase File no puede leer ni escribir datos dentro de un archivo, aunque se puede pasar como referencia a muchas clases de flujos (y métodos) para leer o escribir datos. Para escribir leer datos de un archivo, se utilizan las clases de flujo de E/S: FileInputStream, FileOutputStream, FileReader, FileWriter, RandomAccessFile, etc.

Por ello, se usa para convertir el nombre de un archivo y pasarlo como parámetro a otros métodos o constructores que sí pueden leer o escribir datos.

FileChannel

La clase FileChannel, del API Java NIO, de java.nio.channels proporciona una forma más avanzada de trabajar con archivos que RandomAccessFile. Tanto File como FileChannel funcionan, pero para trabajar con puro Java NIO debe usarse la clase FileChannel.

java.nio.file.Files

La clase java.nio.file.Files proporciona únicamente métodos estáticos para operaciones de archivos y directorios, así como creación de flujos de entrada/salida. Es más eficiente que la clase File y se recomienda su uso en lugar de File para nuevas aplicaciones.

2. Creación de un Objeto File

Un objeto File a menudo se inicializa con una cadena que contiene una ruta absoluta o relativa al archivo o directorio en el sistema de archivos.

La ruta absoluta de un archivo o directorio es la ruta completa desde el directorio raíz hasta el archivo o directorio, incluyendo todos los subdirectorios que contienen el archivo o directorio.

La ruta relativa de un archivo o directorio es la ruta desde el directorio de trabajo actual hasta el archivo o directorio. Por ejemplo, lo siguiente es una ruta absoluta al archivo javaio.txt:

/home/otto/apuntes/javaio.txt

Lo siguiente es una ruta relativa al mismo archivo, asumiendo que el directorio actual del usuario está configurado en /home/otto:

apuntes/javaio.txt

Los sistemas operativos diferentes varían en su formato de nombres de ruta. Por ejemplo, los sistemas basados en Unix usan la barra diagonal hacia adelante, /, para las rutas, mientras que los sistemas basados en Windows usan el carácter diagonal inversa, \, como separador de ruta.

Muchos lenguajes de programación y sistemas de archivos admiten ambos tipos de barras al escribir declaraciones de ruta. Por conveniencia, Java ofrece dos opciones para recuperar el carácter separador local: una propiedad del sistema y una variable estática definida en la clase File. Ambos ejemplos imprimirán el carácter separador para el entorno actual:

System.out.println(System.getProperty("file.separator"));
System.out.println(java.io.File.separator);

Prueba el separador de la plataforma (Gitlab) con el siguiente código:


public class HolaMundo {
    public static void main(String[] args) {
        System.out.println("Sistema: " + System.getProperty("file.separator"));
        System.out.println("Atributo: " + java.io.File.separator);
    }
}
Salida:

El siguiente código crea un objeto File y determina si la ruta a la que hace referencia el archivo existe en el sistema de archivos:

File javaFile = new File("/home/otto/apuntes/javaio.txt");
System.out.println(javaFile.exists()); // true, si el archivo existe

Este ejemplo proporciona la ruta absoluta a un archivo y muestra true o false según si el archivo existe.

Tiene cuatro constructores:

public File(String pathname)
public File(File parent, String child)
public File(String parent, String child)
public File(URI uri)

El primero crea un objeto File a partir de una ruta en forma de cadena. Los otros dos constructores se utilizan para crear un objeto File a partir de una ruta principal y una secundaria, como la siguiente:

File apuntesJavaIO = new File("/home/otto", "apuntes/javaio.md");
File directorioPadre = new File("/home/otto");
File arquivo3 = new File(directorioPadre, "apuntes/javaio.md");

En este ejemplo, creamos dos nuevas instancias de File que son equivalentes la instancia anterior de apuntesJavaIO. Si la instancia principal es nula, se omitiría y el método volvería al constructor de cadena única.

Constructores de la clase File

Constructor Descripción
File(String pathname) Crea un objeto File a partir de una ruta en forma de cadena.
File(File parent, String child) Crea un objeto File a partir de una ruta principal y una secundaria.
File(String parent, String child) Crea un objeto File a partir de una ruta principal y una secundaria.
File(URI uri) Crea un objeto File a partir de un URI.

Campos de la clase File

La clase File tiene varios campos que puedes usar para acceder a información sobre el sistema de archivos subyacente. Algunos de los campos más útiles son:

Campo Descripción
static String pathSeparator El separador de PATH de la plataforma. Por ejemplo, en Windows es ; y en Unix es :.
static char pathSeparatorChar El separador de ruta de la plataforma como un carácter.
static String separator El separador de ruta de la plataforma. Por ejemplo, en Windows es \ y en Unix es /.
static char separatorChar El separador de ruta de la plataforma como un carácter.

3. El objeto File vs. archivo real existente

Al trabajar con una instancia de la clase File, ten en cuenta que sólo representa una ruta a un archivo. A menos que se opere sobre él, no está conectado a un archivo real en el sistema de archivos.

Por ejemplo:

  • Se puede crear un nuevo objeto File para comprobar si un archivo existe en el sistema.
  • Se puede llamar a varios métodos para leer propiedades de archivos dentro del sistema de archivos.
  • Tiene hay métodos para modificar el nombre o la ubicación de un archivo, así como para eliminarlo.

La JVM y el sistema de archivos subyacente leerán o modificarán el archivo utilizando los métodos que llamas en la clase File. Si intentas operar en un archivo que no existe o al que no tienes acceso, algunos métodos de File lanzarán una excepción. Otros métodos devolverán false si el archivo no existe o la operación no se puede realizar.

4. Trabajando con un Objeto File

La clase File contiene numerosos métodos útiles para interactuar con archivos y directorios en el sistema de archivos. En la siguiente tabla se muestran los más importantes, por su uso:

5. Métodos más importantes de java.io.File

Nombre del Método Descripción
boolean delete() Borra el archivo o directorio y devuelve true sólo si la operación se completó con éxito. Si esta instancia es un directorio, el directorio debe estar vacío para poder eliminarse.
boolean exists() Comprueba si un archivo existe
String getAbsolutePath() Obtiene el nombre absoluto del archivo o directorio en el sistema de archivos
String getName() Obtiene el nombre del archivo o directorio
String getParent() Obtiene el directorio principal en el que se encuentra la ruta, o null si no hay ninguno
boolean isDirectory() Comprueba si una referencia File es un directorio en el sistema de archivos
boolean isFile() Comprueba si una referencia File es un archivo en el sistema de archivos
long lastModified() Devuelve el número de milisegundos desde la época (número de milisegundos desde las 12 a.m. UTC del 1 de enero de 1970) en que se modificó el archivo por última vez
long length() Obtiene el número de bytes en el archivo
File[] listFiles() Obtiene una lista de archivos dentro de un directorio
boolean mkdir() Crea el directorio especificado en la ruta
boolean mkdirs() Crea el directorio especificado en la ruta, incluyendo cualquier directorio principal inexistente
boolean renameTo(File dest) Cambia el nombre del archivo o directorio denotado por esta ruta a dest y devuelve true sólo si la operación tuvo éxito.

Prueba el siempre útil programa de ejemplo de muestra que muestra información sobre un archivo o directorio, como si existe, qué archivos contiene y más:

import java.io.*;
import static java.lang.System.out;

public class InfoFile {
    public static void main(String args[]) throws IOException {
        out.print("Raíz del sistema de ficheros");
        for (File raiz: File.listRoots()) {
            out.format("%s ", raiz);
        }
        out.println();
        for (String nome : args) {
            out.format("%n------%new File(%s)%n", nome);
            File f = new File(nome);
            out.format("toString(): %s%n", f);
            out.format("exists(): %b%n", f.exists());
            out.format("lastModified(): %tc%n", f.lastModified());
            out.format("isFile(): %b%n", f.isFile());
            out.format("isDirectory(): %b%n", f.isDirectory());
            out.format("isHidden(): %b%n", f.isHidden());
            out.format("canRead(): %b%n", f.canRead());
            out.format("canWrite(): %b%n", f.canWrite());
            out.format("canExecute(): %b%n", f.canExecute());
            out.format("isAbsolute(): %b%n", f.isAbsolute());
            out.format("length(): %d%n", f.length());
            out.format("getName(): %s%n", f.getName());
            out.format("getPath(): %s%n", f.getPath());
            out.format("getAbsolutePath(): %s%n", f.getAbsolutePath());
            out.format("getCanonicalPath(): %s%n", f.getCanonicalPath());
            out.format("getParent(): %s%n", f.getParent());
            out.format("toURI: %s%n", f.toURI());
        }
    }
}

El siguiente es un programa de ejemplo de muestra que, dado una ruta a un archivo, muestra información sobre el archivo o directorio, si existe, qué archivos contiene y más:

var arquivo = new File("c:\\home\\otto\\noHayCole.txt");
System.out.println("Archivo existe: " + arquivo.exists());
if (arquivo.exists()) {

    System.out.println("Ruta absoluta: " + arquivo.getAbsolutePath());

    System.out.println("Es un directorio: " + arquivo.isDirectory());
    System.out.println("Ruta padre: " + arquivo.getParent());

    if (arquivo.isFile()) {
        System.out.println("Tamaño: " + arquivo.length());
        System.out.println("Última modificación: " + arquivo.lastModified());

    } else {
        for (File subArquivo : arquivo.listFiles()) {
            System.out.println(" " + subArquivo.getName());
        }
    }
}

Si la ruta proporcionada no apuntara a un archivo, produciría la siguiente salida:

Archivo existe: false

Si la ruta proporcionada apuntara a un archivo válido, produciría algo similar a lo siguiente:

Archivo existe: true
Ruta absoluta: c:\home\otto\noHayCole.txt
Es un directorio: false
Ruta padre: c:\home\otto
Tamaño: 14883
Última modificación: 1806860000003

Finalmente, si la ruta proporcionada apuntara a un directorio válido, como c:\home, produciría algo similar a lo siguiente:

Archivo existe: true
Ruta absoluta: c:\home
Es un directorio: true
Ruta padre: c:\
asisoy.txt
noHayCole.txt
zalandomami.txt

En estos ejemplos, ves que la salida de un programa basado en Entrada/Salida depende por completo de los directorios y archivos disponibles en tiempo de ejecución en el sistema de archivos subyacente.

Directorio o archivo

Ojo, /home/otto/noHayCole.txt podría ser un archivo o un directorio, incluso si tiene una extensión de archivo. ¡No asumas que es uno u otro a menos que lo puedas comprobar! (por ejemplo, .git)

Ejercicios

Ejercicio 1. Creación y lectura de archivos con File

Debes trabajar únicamente con métodos de la clase File.

Realiza los siguientes pasos:

  1. Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
  2. Escribe un programa que cree un objeto File para el archivo prueba.txt y compruebe si el archivo existe.
  3. Si el archivo existe, muestra la ruta absoluta, nombre del archivo, tamaño, última modificación y si es un directorio.
  4. Si el archivo no existe, muestra un mensaje que lo indique y crea uno temporal.
Ejercicio 2. Mostrar el contenido de un directorio

Debes trabajar únicamente con métodos de la clase File.

El programa abre una ventana para la selección de un directorio (hazlo también desde teclado si recoge un parámetro) y usando el método listFiles() de la clase File, muestra el contenido de ese directorio, indicando el tamaño de los archivos y si es un directorio o no. Además, muestra el tamaño total de los archivos y directorios.

Muestra en una ventana emergente el resultado y por consola.

A continuación puedes ver algunas soluciones parciales del ejercicio 2. Completa el ejercicio de acuerdo a las indicaciones.

Solución parcial con list()
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        File directorio = new File("C:\\Users\\javhoz\\Documents\\GitHub\\dam2\\");
        File[] archivos = directorio.listFiles();
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
        }
    }
}
Solución parcial con JFileChooser
import javax.swing.JFileChooser;
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        fileChooser.showOpenDialog(null);
        File directorio = fileChooser.getSelectedFile();
        File[] archivos = directorio.listFiles();
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
        }
    }
}
Solución completa con JFileChooser
import javax.swing.JFileChooser;
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        fileChooser.showOpenDialog(null);
        File directorio = fileChooser.getSelectedFile();
        File[] archivos = directorio.listFiles();
        long total = 0;
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
            total += archivo.length();
        }
        System.out.println("Tamaño total: " + total);
    }
}

El siguiente ejemplo muestra cómo mostrar el contenido de un directorio, haciendo uso de la clase que veremos BufferesReader (no Scanner) para la lectura de teclado:

// Programa Java que muestra todo el contenido de un directorio
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;

// Mostrando el contenido de un directorio
class Contents {
    public static void main(String[] args)
            throws IOException
    {
        // Introducimos la ruta y el nombre del directorio por teclado:
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        System.out.println("Introduce la ruta:");
        String dirpath = br.readLine();
        System.out.println("Introduce el nombre del directorio:");
        String dname = br.readLine();

        // creamos un objeto File a partir de la ruta y el nombre del directorio
        File f = new File(dirpath, dname);

        // si el directorio existe, mostramos su contenido
        if (f.exists()) {
            // obtenemos el contenido en un arr[]
            // el array arr[i] representa el nombre cada archivo o directorio
            String arr[] = f.list();

            // Número de entradas en el directorio
            int n = arr.length;

            // mostramos cada una de las entradas.
            for (int i = 0; i < n; i++) {
                System.out.println(arr[i]);
                // Creamos un objeto File para cada entrada y 
                // comprobamos si es un archivo o un directorio.
                File f1 = new File(arr[i]);
                if (f1.isFile())
                    System.out.println(": es un archivo");
                if (f1.isDirectory())
                    System.out.println(": es un directorio");
            }
            System.out.println("El directorio no tiene entradas " + n);
        }
        else
            System.out.println("Directorio no encontrado");
    }
}
Ejercicio 3. Gestor de archivos y directorios

Como en todos los ejercicios anteriores, debes trabajar únicamente con métodos de la clase File.

Escribe un programa en Java que funcione como un gestor básico de archivos y directorios. El programa debe permitir al usuario realizar las siguientes operaciones:

  1. Crear un directorio, empleando la clase JFileChooser para seleccionar la ruta donde se creará.
  2. Listar todos los archivos y subdirectorios de un directorio de forma recursiva.
  3. Eliminar un archivo o directorio. Si es un directorio, eliminar todo su contenido de forma recursiva.
  4. Mover o renombrar archivos y directorios.

El programa debe ofrecer un menú para que el usuario elija la operación que desea realizar. La selección de directorios o archivos debe realizarse con la clase JFileChooser.

6. Nuevas características del paquete java.nio.file

Aunque la clase java.io.File es útil para muchas operaciones de E/S de archivos, Java SE 7 introdujo una nueva API de E/S de archivos en el paquete java.nio.file que proporciona una funcionalidad más rica y más eficiente para trabajar con archivos y directorios. Este modo de hacerlo lo veremos en el siguiente apartado.

Antes del lanzamiento de Java SE 7, la clase java.io.File era el mecanismo utilizado para la E/S de archivos, pero presentaba varios inconvenientes:

  • Muchos métodos no lanzaban excepciones al fallar, por lo que era imposible obtener un mensaje de error útil. Por ejemplo, si fallaba la eliminación de un archivo, el programa recibía un “fallo al eliminar”, pero no sabía si era porque el archivo no existía, el usuario no tenía permisos, o había algún otro problema.
  • El método rename no funcionaba de manera consistente en diferentes plataformas.
  • No había un soporte real para enlaces simbólicos.
  • Se requería más soporte para metadatos, como permisos de archivos, propietario del archivo y otros atributos de seguridad.
  • El acceso a los metadatos de los archivos era ineficiente.
  • Muchos de los métodos no escalaban bien. Solicitar un listado de directorios grandes en un servidor podía causar bloqueos. Los directorios grandes también podían generar problemas de recursos de memoria, lo que resultaba en una denegación de servicio.
  • No era posible escribir código fiable que pudiera recorrer un árbol de archivos recursivamente y responder adecuadamente si había enlaces simbólicos circulares.

Aun así, existe mucho código que usa java.io.File y sigue siendo útil para muchas situaciones. Aunque lo veremos al detalle, si quisieras aprovechar la funcionalidad de java.nio.file.Path con el menor impacto posible en tu código muestro ejemplos de ello.

Conversión entre java.io.File y java.nio.file.Path

La clase java.io.File proporciona el método toPath, que convierte una instancia de estilo antiguo en una instancia java.nio.file.Path:

Path entrada = file.toPath();

De esta forma, puedes aprovechar el conjunto de características que ofrece la clase Path.

Por ejemplo, si tuvieras algún código que eliminara un archivo:

file.delete();

Podrías modificar este código para usar el método Files.delete, de la siguiente manera:

Path fp = file.toPath();
Files.delete(fp);

A la inversa, el método Path.toFile construye un objeto java.io.File para un objeto Path.

Mapeo de la Funcionalidad de java.io.File a java.nio.file

Dado que la implementación de la E/S de archivos en Java ha sido completamente re-arquitectada en la versión Java SE 7, no puedes intercambiar un método por otro directamente. Si deseas usar la rica funcionalidad que ofrece el paquete java.nio.file, la solución más sencilla es usar el método File.toPath.

No hay una correspondencia uno a uno entre las dos APIs, pero la siguiente tabla da una idea general de qué funcionalidad en la API java.io.File corresponde a la funcionalidad en la API java.nio.file, y te indica dónde puedes obtener más información.

Funcionalidad de java.io.File Funcionalidad de java.nio.file Uso
java.io.File java.nio.file.Path Clase principal de gestión de archivos.
java.io.RandomAccessFile SeekableByteChannel Archivos de Acceso Aleatorio
File.canRead, canWrite, canExecute Files.isReadable, Files.isWritable, Files.isExecutable Verificación de archivo o directorio.
File.isDirectory(), File.isFile(), File.length() Files.isDirectory(Path, LinkOption...), Files.isRegularFile(Path, LinkOption...), Files.size(Path) Gestión de Metadatos de archivo/directorio.
File.lastModified(), File.setLastModified(long) Files.getLastModifiedTime(Path, LinkOption...), Files.setLastModifiedTime(Path, FileTime) Gestión de Metadatos de fecha modificación.
Métodos que establecen varios atributos (setExecutable, setReadable, setReadOnly, setWritable) Files.setAttribute(Path, String, Object, LinkOption...) Gestión de Metadatos de atributos de archivo.
new File(parent, "newfile") parent.resolve("newfile") Operaciones con archivos
File.renameTo Files.move Mover un Archivo o Directorio
File.delete Files.delete Eliminar un Archivo o Directorio
File.createNewFile Files.createFile Crear Archivos
File.deleteOnExit Opción DELETE_ON_CLOSE especificada en createFile Borrado de archivos al salir.
File.createTempFile Files.createTempFile(Path, String, FileAttributes<?>), Files.createTempFile(Path, String, String, FileAttributes<?>) Crear Archivos temporales.
File.exists Files.exists, Files.notExists Verificar la Existencia de un Archivo o Directorio
File.compareTo, equals Path.compareTo, equals Comparar dos archivos/paths
File.getAbsolutePath, getAbsoluteFile Path.toAbsolutePath Obtención de la ruta absoluta.
File.getCanonicalPath, getCanonicalFile Path.toRealPath o normalize Convertir un Path (toRealPath), Eliminar Redundancias en un Path (normalize)
File.toURI Path.toURI Convertir un path en una URL.
File.isHidden Files.isHidden Saber si está oculto.
File.list, listFiles Path.newDirectoryStream Listar el Contenido de un Directorio
File.mkdir, mkdirs Files.createDirectory Crear directorio/s
File.listRoots FileSystem.getRootDirectories Listar los Directorios Raíz del Sistema de Archivos
File.getTotalSpace, getFreeSpace, getUsableSpace FileStore.getTotalSpace, getUnallocatedSpace, getUsableSpace, getTotalSpace Atributos del Almacenamiento de Archivos

7. La clase java.io.RandomAccessFile

La clase RandomAccessFile permite acceso no secuencial, o aleatorio, al contenido del archivo.

Permite leer y escribir (implementa las interfaces DataInput y DataOutput) en archivos de acceso aleatorio. En el constructor se especifica el modo de apertura, lectura o escritura:

new RandomAccessFile("proba.txt", "r");  // Solo lectura
new RandomAccessFile("proba.txt", "rw");  // Lectura y escritura
new RandomAccessFile("proba.txt", "rwd"); // Lectura y escritura, sincronizado
  • Emplea la notación de puntero a archivo para especificar la posición actual en el archivo.

  • Al crearlo apunta al principio del archivo, la posición 0.

Las sucesivas llamadas a read o write modifican la posición del puntero el número de bytes leídos o escritos, respectivamente.

Dispone de 3 métodos para modificar la posición del puntero:

  • int skipBytes(int n): mueve el puntero hacia delante n bytes.
  • void seek(long): sitúa el puntero justo antes del byte especificado.
  • long getFilePointer(): devuelve la posición actual del puntero a archico.

Definición de la clase RandomAccessFile:

public class RandomAccessFile  
        extends Object implements DataOutput, DataInput, Closeable

Las instancias de esta clase soportan tanto la lectura como la escritura en un archivo de acceso aleatorio. Un archivo de acceso aleatorio se comporta como un gran array de bytes almacenado en el sistema de archivos. Existe un tipo de cursor, o índice en el array implícito, llamado puntero de archivo; las operaciones de entrada leen bytes comenzando en el puntero de archivo y avanzan el puntero más allá de los bytes leídos.

Si el archivo de acceso aleatorio se crea en modo de lectura/escritura, entonces las operaciones de salida también están disponibles; las operaciones de salida escriben bytes comenzando en el puntero de archivo y avanzan el puntero más allá de los bytes escritos. Las operaciones de salida que escriben más allá del final actual del array implícito causan que el array se extienda.

El puntero de archivo se puede leer mediante el método getFilePointer y establecer mediante el método seek.

Para todas las rutinas de lectura en esta clase que, si se alcanza el final del archivo antes de que se haya leído el número deseado de bytes, se lanza una excepción EOFException (que es un tipo de IOException).

Si no se puede leer ningún byte por alguna razón que no sea el final del archivo, se lanza una IOException distinta a EOFException. En particular, puede lanzarse una IOException si el flujo ha sido cerrado.

Ejemplo de uso de la clase RandomAccessFile:

import java.io.*;

public class RandomAccessFileDemo {
    public static void main(String[] args) {
        try {
            RandomAccessFile raf = new RandomAccessFile("proba.txt", "rw");
            raf.writeUTF("Hola, mundo!");
            raf.seek(0);
            System.out.println(raf.readUTF());
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Escritura con RandomAccessFile

Ahora veremos cómo escribir y editar dentro de un archivo existente, en lugar de solo escribir en un archivo completamente nuevo o agregar a uno existente. Simplemente: necesitamos acceso aleatorio.

RandomAccessFile nos permite escribir en una posición específica del archivo, dado el desplazamiento (offset) desde el principio del archivo en bytes.

Este código escribe un valor entero con un desplazamiento dado desde el principio del archivo:

private void writeToPosition(String filename, int data, long position) 
  throws IOException {
    RandomAccessFile writer = new RandomAccessFile(filename, "rw");
    writer.seek(position);
    writer.writeInt(data);
    writer.close();
}

Si queremos leer el entero almacenado en una ubicación específica, podemos usar este método:

private int readFromPosition(String filename, long position) 
  throws IOException {
    int result = 0;
    RandomAccessFile reader = new RandomAccessFile(filename, "r");
    reader.seek(position);
    result = reader.readInt();
    reader.close();
    return result;
}

Para probar nuestras funciones, escribamos un entero, lo editemos, y finalmente lo leamos:

@Test
public void whenWritingToSpecificPositionInFile_thenCorrect() 
  throws IOException {
    int data1 = 2014;
    int data2 = 1500;
    
    writeToPosition(fileName, data1, 4);
    assertEquals(data1, readFromPosition(fileName, 4));
    
    writeToPosition(fileName2, data2, 4);
    assertEquals(data2, readFromPosition(fileName, 4));
}

Ejercicios

Ejercicio 4. Escritura y lectura de archivos con RandomAccessFile

Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile.

  1. Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
  2. Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
  3. Lee el mensaje y muéstralo por consola.
Ejercicio 5. Escritura y lectura de archivos con RandomAccessFile

Escribe un programa que utilice la clase RandomAccessFile para escribir en un archivo los números del 1 al 10 y luego los lea desde el archivo. Muestra los números leídos en la consola.

Solución al ejercicio 5
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileDemo {
    public static void main(String[] args) {
        try {
            RandomAccessFile raf = new RandomAccessFile("prueba.txt", "rw");
            for (int i = 1; i <= 10; i++) {
                raf.writeInt(i);
            }
            raf.seek(0);
            for (int i = 1; i <= 10; i++) {
                System.out.println(raf.readInt());
            }
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
    
        }
    }
}
Ejercicio 6. Modificación de Contenido en un Archivo Binario con RandomAccessFile

Escribe un programa en Java que haga lo siguiente:

  • Escriba 10 enteros en un archivo llamado “datos.bin”.
  • Permita al usuario modificar el tercer número almacenado en el archivo por otro número.
  • Muestra los números antes y después de la modificación en la consola.
Solución al ejercicio 6
import java.io.RandomAccessFile;
import java.io.IOException;
import java.util.Scanner;

public class Ejercicio3 {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("datos.bin", "rw")) {
            // Escribir 10 enteros en el archivo
            for (int i = 1; i <= 10; i++) {
                raf.writeInt(i);
            }

            // Leer los números antes de la modificación
            System.out.println("Números antes de la modificación:");
            raf.seek(0);
            for (int i = 0; i < 10; i++) {
                System.out.println(raf.readInt());
            }

            // Solicitar al usuario un nuevo número para el tercer número
            Scanner sc = new Scanner(System.in);
            System.out.print("Ingrese un nuevo número para reemplazar el tercer número: ");
            int nuevoNumero = sc.nextInt();

            // Modificar el tercer número (posición 2 en base 0, cada entero ocupa 4 bytes)
            raf.seek(2 * 4);
            raf.writeInt(nuevoNumero);

            // Leer los números después de la modificación
            System.out.println("Números después de la modificación:");
            raf.seek(0);
            for (int i = 0; i < 10; i++) {
                System.out.println(raf.readInt());
            }

        } catch (IOException e) {
            System.out.println("Ocurrió un error de entrada/salida.");
            e.printStackTrace();
        }
    }
}
Última actualización: 23.09.2025

01.02 La clase RandomAccessFile

La Clase RandomAccessFile en Java

La clase RandomAccessFile de Java en la API de Java IO te permite navegar por un archivo y leer o escribir en él según sea necesario. También puedes reemplazar partes existentes de un archivo. Esto no es posible con FileInputStream o FileOutputStream, que veremos en el apartado de flujos de E/S.

1. Creación de un RandomAccessFile

Antes de poder trabajar con la clase RandomAccessFile, debes crear una instancia de esa clase:

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");

Nota el segundo parámetro del constructor, "rw",es el modo en el que quieres abrir el archivo. "rw" significa modo de lectura/escritura..

2. Modos de Acceso

La clase RandomAccessFile de Java soporta los siguientes modos de acceso:

Modo Descripción
r Modo de lectura. Llamar a los métodos de escritura lanzará en una IOException.
rw Modo de lectura y escritura.
rwd Modo de lectura y escritura - sincrónicamente. Todas las actualizaciones al contenido del archivo se escriben en el disco de manera sincrónica.
rws Modo de lectura y escritura - sincrónicamente. Todas las actualizaciones al contenido del archivo o metadatos se escriben en el disco de manera sincrónica.

3. Situar el puntero: seek()

Para leer o escribir en una ubicación específica en un RandomAccessFile, primero debes situar el puntero del archivo (también llamado seek) en la posición de lectura o escritura. Esto se hace utilizando el método seek(). Por ejemplo:

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");
file.seek(100);

4. Posición actual del puntero: getFilePointer()

Puedes obtener la posición actual de un RandomAccessFile usando su método getFilePointer(). La posición actual es el índice (desplazamiento) del byte en el que el RandomAccessFile está actualmente situado:

long posicion = file.getFilePointer();

5. Lectura de un Byte desde: read()

La lectura un byte desde un RandomAccessFile se realiza usando su método read():

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");
int miByte = file.read();

El método read() lee el byte ubicado en la posición del archivo señalada por el puntero en la instancia de RandomAccessFile.

Avance del puntero

Un detalle que el javadoc olvida mencionar: el método read() incrementa el puntero del archivo para que apunte al siguiente byte después del que acaba de ser leído. Esto significa se puede seguir llamando a read() sin tener que mover manualmente el puntero del archivo.

6. Lectura de un array de bytes: read(byte[])

También es posible leer un array de bytes con un RandomAccessFile:

RandomAccessFile randomAccessFile = new RandomAccessFile("programas/datos.txt", "r");

byte[] dest      = new byte[1024]; // Array de bytes donde se almacenarán los datos leídos, llamado buffer.
int    offset    = 0;
int    length    = 1024;
int    bytesLeidos = randomAccessFile.read(dest, offset, length);

Este ejemplo lee una secuencia de bytes en el array de bytes dest pasado como parámetro al método read(). El método read() comenzará a leer en el archivo desde la posición actual del puntero del archivo en el RandomAccessFile. El método read() comenzará a leer datos en el array de bytes a partir de la posición proporcionada por el parámetro offset, y como máximo el número de bytes proporcionado por el parámetro length.

Este método devuelve el número real de bytes leídos.

7. Escritura de un byte: write()

Puedes escribir un byte en un RandomAccessFile usando su método write(), el cual toma un entero como parámetro. El byte se escribirá en la posición actual del puntero del archivo en el RandomAccessFile. El byte anterior en esa posición será sobrescrito:

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");
file.write(67); // Código ASCII para 'C'

Recuerda, llamar a este método write() avanzará la posición del archivo en 1 byte, al igual que sucede con el método read().

8. Escritura de un array de bytes: write(byte[])

Escribir en un RandomAccessFile se puede hacer usando uno de sus muchos métodos write():

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");

byte[] bytes = "Hello World".getBytes("UTF-8");
file.write(bytes);

Este ejemplo escribe el array de bytes en la posición actual del puntero del archivo en el objeto RandomAccessFile. Cualquier byte que esté en esa posición será sobrescrito con los nuevos bytes.

Al igual que con el método read(), el método write() avanza el puntero del archivo después de ser llamado. De esta manera no tienes que mover constantemente el puntero para escribir datos en una nueva ubicación en el archivo.

También puedes escribir partes de un array de bytes en un RandomAccessFile, en lugar de todo el array:

RandomAccessFile file = new RandomAccessFile("c:\\data\\holamundo.kt", "rw");

byte[] bytes = "Hello World".getBytes("UTF-8");
file.write(bytes, 2, 5);

Este ejemplo escribe desde el desplazamiento (offset) 2 del array de bytes y 5 bytes hacia adelante, longitud (length).

9. Cierre del archivo

El RandomAccessFile tiene un método close() que debe ser llamado cuando hayas terminado de usar la instancia de RandomAccessFile:

RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw");
file.close();

También puedes cerrar un RandomAccessFile automáticamente si usas la sentencia try-with-resources de Java:

try (RandomAccessFile file = new RandomAccessFile("c:\\programas\\holamundo.kt", "rw")) {

    // lectura o escritura en el RandomAccessFile

}

Una vez que la ejecución del programa salga del bloque try-with-resources, el objeto RandomAccessFile se cerrará automáticamente, incluso si se lanza una IOException desde dentro del bloque try-with-resources.

Ejemplo completo del uso de RandomAccessFile

En el siguiente ejemplo escribimos una lista de estudiantes pedidos por teclado, guardando el número de estudiantes y el nombre en el mismo archivo. Para la lectura solicitamos el número del estudiante a leer:

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Scanner;

public class RegistroEstudiantes {

    public static void main(String[] args) throws IOException { // En realidad es mala opción lanzar la excepción, pero es para simplificar el ejemplo

        try (RandomAccessFile file = new RandomAccessFile("E:\\programas\\estudiantes.txt", "rw")) {

            Scanner scanner = new Scanner(System.in);

            System.out.println("Introduce el número de estudiantes: ");
            int numEstudiantes = scanner.nextInt();
            file.writeInt(numEstudiantes);

            for (int i = 0; i < numEstudiantes; i++) {
                System.out.println("Introduce el nombre del estudiante " + (i + 1) + ": ");
                String nombre = scanner.next();
                file.writeUTF(nombre);
            }

            System.out.println("Introduce el número del estudiante a leer: ");
            int numEstudiante = scanner.nextInt();

            file.seek(0);
            int numEstudiantesGuardados = file.readInt();

            if (numEstudiante > numEstudiantesGuardados) {
                System.out.println("No hay tantos estudiantes guardados.");
            } else {
                file.seek(4); // Saltamos el número de estudiantes
                for (int i = 0; i < numEstudiante - 1; i++) {
                    file.readUTF();
                }
                System.out.println("El estudiante " + numEstudiante + " es: " + file.readUTF());
            }
        }
    }
}
Última actualización: 23.09.2025

01.03 Flujos de E/S


1. Introducción a los flujos de E/S

Ahora que hemos cubierto los conceptos básicos de la clase File, pasemos a los flujos (streams) de E/S, que son mucho más interesantes, pues no sólo pueden emplearse para archivos.

Un flujo de E/S representa una fuente de entrada o un destino de salida. Un flujo puede representar muchos tipos diferentes de fuentes y destinos, incluidos archivos en disco, dispositivos, otros programas, String o arrays de memoria.

En esta sección, veremos cómo usar los flujos de E/S para leer y escribir datos. La “E/S” se refiere a la naturaleza de cómo se accede a los datos, ya sea leyendo los datos desde un recurso (entrada) o escribiendo los datos en un recurso (salida).

Flujos de E/S en Java

En Java, los flujos de E/S se encuentran en el paquete java.io. Aunque Java 9 introdujo un nuevo paquete java.nio.file para operaciones de E/S más avanzadas, java.io sigue siendo ampliamente utilizado y es importante comprenderlo.

Los flujos admiten muchos tipos diferentes de datos, incluidos bytes simples, tipos de datos primitivos, caracteres localizados y objetos. Algunos flujos simplemente transmiten datos; otros manipulan y transforman los datos de formas útiles.

Flujo de entrada

Flujo de entrada Flujo de entrada

Representan una fuente de entrada. Pueden proceder de diferentes tipos de fuentes:

Flujo de salida

Flujo de salida Flujo de salida

Tipos de datos

Ambos tipos de flujo pueden representar diferentes tipos de datos:

  • Bytes simples. (FileInputStream, FileOutputStream,…).
  • Tipos de datos primitivos (DataInputStream,…).
  • Caracteres (FileReader, FileWriter,…).
  • Objetos (ObjectInputStream,…).
  • Algunos flujos simplemente pasan datos, otros manipulan y transforman los datos.

2. Fundamentos de los flujos de E/S

El contenido de un archivo, una página Web, el teclado, etc. se puede leer o escribir a través de un flujo, que es una lista de elementos de datos presentados secuencialmente. Deberías pensar en los flujos conceptualmente como un “flujo de agua” largo y casi interminable con datos que se presentan uno a uno, como una “ola” a la vez.

En general, el flujo es tan grande que una vez que comenzamos a leerlo, no tenemos idea de dónde comienza o termina. Sólo tenemos un puntero a nuestra posición actual en el flujo y leemos datos bloque por bloque.

Cada tipo de flujo segmenta los datos en una “chorro” o “bloque” de una manera particular. Por ejemplo, algunas clases de flujos leen o escriben datos como bytes individuales. Otras clases de flujos leen o escriben caracteres individuales o cadenas de caracteres. Además, algunas clases de flujos leen o escriben grupos más grandes de bytes o caracteres a la vez, específicamente aquellas con la palabra “Buffered” en su nombre.

Flujos Flujos

Aunque los flujos se utilizan comúnmente con la E/S de archivos, se utilizan de manera más general para manejar la lectura/escritura de cualquier fuente de datos de flujos. Por ejemplo, podrías construir una aplicación Java que envíe datos a un sitio web utilizando un flujo de salida y lea el resultado a través de un flujo de entrada.

Entrada vs Salida

Es importante distinguir entre entrada (InputStream/Reader) y salida (OutputStream/Writer). Es muy sencillo, pues siempre debe verse desde el punto de vista del programa: entrada de datos al programa (lectura) y salida de datos desde el programa (escritura).

3. Nomenclatura de los flujos de E/S

La API java.io proporciona numerosas clases para crear, acceder y manipular flujos, tantas que tienden a abrumar a muchos desarrolladores de Java. ¡Mantén la calma! ;-) Revisaremos las principales diferencias entre cada clase de flujo y veremos cómo distinguirlas. A menudo el nombre del flujo te proporciona suficiente información para comprender exactamente qué hace.

El objetivo de este apartado es familiarizarte con la terminología común y las convenciones de nombres utilizadas con los flujos. No te preocupes si no reconoces los nombres de las clases de flujos particulares que se utilizan aquí; los veremos más adelante y con la práctica se entenderá mejor.

4. Flujos de bytes vs. flujos de caracteres

La API java.io define dos conjuntos de clases de flujos para la lectura y escritura de flujos: flujos de bytes y flujos de caracteres.

4.1. Flujos de bytes (Byte Streams)

  • Los flujos de bytes leen/escriben datos binarios (0 y 1) y tienen nombres de clase que terminan en InputStream o OutputStream.
  • Todas las clases descienden (heredan) de InputStream y OutputStream.
  • Hay muchas clases de flujos de bytes, como: FileInputStream y FileOutputStream. Todos los restantes flujos funcionan del mismo modo sólo difieren en la forma de construirlos.

Los programas utilizan flujos de bytes para realizar la entrada y salida de bytes de 8 bits. Todas las clases de flujos de bytes heredan de InputStream y OutputStream.

4.2. Flujos de caracteres (Character Streams)

  • Los flujos de caracteres leen/escriben datos de texto y tienen nombres de clase que terminan en Reader o Writer.
  • Automáticamente, transforma caracteres Unicode (formato de Java) al conjunto de caracteres local.
  • Todas las clases descienden de Reader y Writer.
  • Hay muchas clases de flujos de carácter, como :FileReader (usa internamente FileInputStream), FileWriter (usa internamente FileOutpuStream). Todos los restantes flujos funcionan de igual modo, sólo difieren en la forma de construirlos.

Java almacena valores de caracteres utilizando convenciones Unicode. La E/S de flujos de caracteres traduce automáticamente este formato interno hacia y desde el conjunto de caracteres local. En locales occidentales, como el juego de caracteres Latin-1 o Windows-1252, el conjunto de caracteres local es generalmente un superconjunto de ASCII de 8 bits. En locales asiáticos, el conjunto de caracteres local es un conjunto de caracteres de doble byte.

Esquema de flujos Esquema de flujos

5. Flujos de entrada (Input Streams) vs. flujos de salida (Output Streams)

La mayoría de las clases de flujos de entrada tienen una clase de flujo de salida correspondiente, y viceversa. Por ejemplo, la clase FileOutputStream escribe datos que pueden ser leídos por un FileInputStream. Si comprendes las características de una clase de flujo de entrada o salida en particular, naturalmente sabrás qué hace su clase complementaria.

Por lo tanto, la mayoría de las clases Reader tienen una clase Writer correspondiente. Por ejemplo, la clase FileWriter escribe datos que pueden ser leídos por un FileReader. Aunque hay excepciones a esta regla:

La soledad de los flujos de salida PrintWriter y PrintStream

Debes saber que [PrintWriter][printwriter] no tiene una clase PrintReader correspondiente.

Del mismo modo, PrintStream es una OutputStream que no tiene una clase InputStream correspondiente. Tampoco tiene la palabra “Output” en su nombre.

El principal propósito de PrintWriter y PrintStream es facilitar la escritura de datos formateados en un flujo, como sucede con System.out y System.err que son de tipo PrintStream.

Hablaremos de estas clases más adelante.

Última actualización: 23.09.2025

01.04 Flujos de byte

Flujos de bytes (Byte Streams)

  • Los flujos de bytes leen/escriben datos binarios (0 y 1) y tienen nombres de clase que terminan en InputStream o OutputStream.
  • Leen en bloques de bytes y no pueden manejar caracteres Unicode.
  • Todas las clases descienden (heredan) de InputStream y OutputStream.
  • Hay muchas clases de flujos de bytes, como: FileInputStream y FileOutputStream. Todos los restantes flujos funcionan del mismo modo sólo difieren en la forma de construirlos.

Los programas utilizan flujos de bytes para realizar la entrada y salida de bytes de 8 bits. Todas las clases de flujos de bytes heredan de InputStream y OutputStream.

Veremos un ejemplo de cómo funcionan los flujos de bytes con flujos de bytes de E/S de archivo, FileInputStream y FileOutputStream. Otros tipos de flujos de bytes se utilizan de manera muy similar; difieren principalmente en la forma en que se construyen.

Ejemplo: copia de archivos

Programa que emplea FileInputStream y FileOutputStream para copiar archivos CopiaArchivos, que utiliza flujos de bytes para copiar un byte a la vez.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopiaArchivos {
    public static void main(String[] args) throws IOException {

        FileInputStream in = null;
        FileOutputStream out = null;

        try {
            in = new FileInputStream("otto.txt");
            out = new FileOutputStream("nohaycole.txt");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        } finally { // Hay que cerrar el flujo en cualquier condición.
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

CopiaArchivos lee el flujo de entrada y escribe el flujo de salida, un byte a la vez.

Copia de archivos Copia de archivos

  • El método read() devuelve un valor de byte en forma de un entero, para poder emplear -1 como fin de flujo. Cuando se alcanza el final del archivo, read() devuelve -1.

  • El método write() escribe un byte en el flujo de salida.

  • El método close() cierra el flujo. Si no se cierra, el sistema operativo puede no liberar los recursos asociados con el archivo.

  • Para ficheros de texto (con caracteres, como en el ejemplo) es mejor emplear flujos de caracteres (character streams).

  • Los flujos de bytes deben usarse sólo para E/S más primitiva (binaria)

  • Todos los otros tipos de flujos (incluso caracteres) se construyen sobre los flujos de bytes.

Copia de archivos

CopiaArchivos parece un programa normal, pero en realidad representa un tipo de E/S de bajo nivel que debería evitar. Dado que otto.txt contiene datos de caracteres, el mejor enfoque es usar flujos de caracteres, como veremos más adelante. También hay flujos para tipos de datos más complejos. Los flujos de bytes solo deben usarse para la E/S más primitiva.

Entonces, ¿por qué hablar de flujos de bytes? Porque todos los demás tipos de flujos se construyen sobre flujos de bytes.

Ejercicio 1. Copia de archivos

Modifica el programa CopiaArchivos para que copie el archivo otto.txt en un archivo nohaycole.txt en la carpeta src/main/resources de tu proyecto.

Además, haz que el cierre de archivos se realice por medio de try-with-resources.

Cierre de flujos

Cerrar un flujo cuando ya no se necesita es muy importante. CopiaArchivos utiliza un bloque finally para garantizar que ambos flujos se cierren incluso si se produce un error. Esta práctica ayuda a evitar graves pérdidas de recursos.

La técnica más recomendada es utilizar try-with-resources, que permite que los flujos se cierren automáticamente al final del bloque try:

try (FileInputStream in = new FileInputStream("otto.txt");
     FileOutputStream out = new FileOutputStream("nohaycole.txt")) {
    int c;
    while ((c = in.read()) != -1) {
        out.write(c);
    }
}

Un posible error es que CopiaArchivos no pudo abrir uno o ambos archivos. Cuando esto sucede, la variable de flujo correspondiente al archivo nunca cambia desde su valor inicial nulo. Es por eso que CopiaArchivos se asegura de que cada variable de flujo contenga una referencia de objeto antes de llamar a close().

Cuando no usar flujos de bytes

CopiaArchivos parece un programa normal, pero en realidad representa un tipo de E/S de bajo nivel que debería evitar. Dado que otto.txt contiene datos de caracteres, el mejor enfoque es usar flujos de caracteres, como veremos más adelante. También hay flujos para tipos de datos más complejos. Los flujos de bytes solo deben usarse para la E/S más primitiva.

Entonces, ¿por qué hablar de flujos de bytes? Porque todos los demás tipos de flujos se construyen sobre flujos de bytes.

1. InputStream

Flujos de entrada que heredan de InputStream, que es abstracta:

  • ByteArrayInputStream: contiene un búfer interno que contiene bytes que pueden ser leídos desde el flujo. Un contador interno lleva un seguimiento del próximo byte que será suministrado por el método read. Cerrar un ByteArrayInputStream no tiene efecto. Los métodos en esta clase pueden ser llamados después de que el flujo haya sido cerrado sin generar una IOException.

  • FileInputStream

  • AudioInputStream: es un flujo de entrada con un formato de audio y longitud especificados. La longitud se expresa en frames, no en bytes. Se proporcionan varios métodos para leer un cierto número de bytes del flujo, o un número no especificado de bytes. El flujo de entrada de audio lleva un seguimiento del último byte que se leyó. Puedes saltar sobre un número arbitrario de bytes para llegar a una posición posterior para la lectura. Un flujo de entrada de audio puede admitir marcas. Cuando estableces una marca, se recuerda la posición actual para que puedas volver a ella más tarde. La clase AudioSystem incluye muchos métodos que manipulan objetos AudioInputStream. Por ejemplo, los métodos te permiten:

    • Obtener un flujo de entrada de audio desde un archivo de audio externo, un flujo o una URL.
      • Escribir un archivo externo desde un flujo de entrada de audio.
      • Convertir un flujo de entrada de audio a un formato de audio diferente.
  • FilterInputStream: encapsula otro flujo de entrada y proporciona funcionalidad adicional. Ejemplo:

  • ObjectInputStream: lee objetos Java serializados del flujo de entrada.

  • PipedInputStream: implementa un tubo de entrada.

  • SequenceInputStream: concatena dos flujos de entrada.

  • StringBufferInputStream: desaprobada. Se recomienda el uso de StringReader.

2. OutputStream

Flujos de salida que heredan de OutputStream, que es abstracta:

  • ByteArrayOutputStream: implementa un flujo de salida en el que los datos se escriben en un array de bytes. El búfer crece automáticamente a medida que se escriben datos en él. Los datos se pueden recuperar usando toByteArray() y toString(). Cerrar a ByteArrayOutputStream no tiene ningún efecto. Los métodos de esta clase se pueden llamar después de que se haya cerrado la secuencia sin generar un archivo IOException.
  • FileOutputStream: flujo de salida para escribir datos en un archivo File o en un archivo FileDescriptor. El hecho de que un archivo esté disponible o pueda crearse depende de la plataforma subyacente. Algunas plataformas, en particular, permiten que un archivo sea abierto para escritura por solo uno FileOutputStream (u otro objeto de escritura de archivos) a la vez. En tales situaciones, los constructores de esta clase fallarán si el archivo involucrado ya está abierto. FileOutputStream está destinado a escribir flujos de bytes sin formato, como datos de imágenes. Para escribir secuencias de caracteres debe usarse el orientado a carácter FileWriter.
  • ObjectOutputStream: escribe objetos Java serializados en un flujo de salida.
  • PipedOutputStream: implementa un tubo de salida.
  • FilterOutputStream: encapsula otro flujo de salida y proporciona funcionalidad adicional. Ejemplo:
    • BufferedOutputStream: escribe bytes en un flujo de salida y los almacena en un búfer interno.
    • PrintStream: proporciona métodos para imprimir representaciones de datos primitivos y objetos en un flujo de salida. un ejemplo de uso es System.out.
    • CheckedOutputStream: calcula un valor de comprobación de suma de verificación (checksum)para los datos escritos en el flujo de salida. Se puede emplear para comprobar la integridad de los datos de salida.
    • CipherOutputStream: escribe datos cifrados en un flujo de salida. Está compuesto por un OutputStream y un objeto de tipo Cipher, para procesar los datos antes de escribirlos en el flujo de salida. Debe ser inicializado con un modo de cifrado y una clave.
    • DataOutputStream: escribe datos primitivos Java en el flujo de salida. Los datos se pueden recuperar usando DataInputStream.
    • DeflaterOutputStream: comprime los datos escritos en el flujo de salida. Tiene dos subclases: GZIPOutputStream y ZipOutputStream.
    • DigestOutputStream: calcula un resumen de mensaje de los datos escritos en el flujo de salida. Se puede emplear para comprobar la integridad de los datos de salida.
    • InflaterOutputStream: implanta un filtro de flujo de salida para descomprimir datos comprimidos en formato de compresión de “deflate”.

3. ObjectInputStream y ObjectOutputStream

ObjectInputStream: lee objetos Java serializados del flujo de entrada y los deserializa. ObjectOutputStream: escribe objetos Java serializados en un flujo de salida.

ObjectInputStream y ObjectOutputStream ObjectInputStream y ObjectOutputStream

Para emplear las clases ObjectInputStream, ObjectOutStream los objetos a leer (escribir deben implantar la interface: Serializable (dicha interface no tiene métodos para implantar)

Para escribir:

Object ob = new Object();
out.writeObject(ob);  //out es un flujo de tipo ObjectOutputStream
out.writeObject(ob); 

Para leer:

Object ob1 = in.readObject();
Object ob2 = in.readObject(); 
Serialización

La serialización es el proceso de convertir un objeto en una secuencia de bytes que se pueden escribir en un flujo de salida y, posteriormente, reconstruir el objeto a partir de esos bytes. La deserialización es el proceso inverso: reconstruir un objeto a partir de una secuencia de bytes.

Ejercicio 2. Serialización

Crea una clase Persona con los atributos nombre y edad. Crea un programa que serialice y deserialice un objeto de tipo Persona.

Debe tener un menú con las siguientes opciones:

  1. Añadir persona.
  2. Mostrar personas.
  3. Buscar persona (por número o por nombre, según consideres)
  4. Salir

Puedes hacerlo desde consola o por medio de una interfaz gráfica, haciendo uso de JOptionPane para introducir los datos (JOptionPane.showInputDialog) y mostrar los resultados (JOptionPane.showMessageDialog).

Ejercicio 3. Serialización de colecciones

Crea una clase ColeccionPersonas que contenga una colección de objetos de tipo Persona. Implementa la interface Serializable y crea un programa que serialice y deserialice un objeto de tipo ColeccionPersonas.

4. Lectura desde URL

Para leer desde una URL, se puede emplear la clase URL y openStream():

import java.io.*;

public class LeerURL {
    public static void main(String[] args) throws Exception {
        // URL url = new URL("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/"); // Desaprobado.
        // Versión actualizada:
        URI uri = new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
        URL url = uri.toURL();
        
        try (InputStream is = url.openStream();
             InputStreamReader isr = new InputStreamReader(is); // es un puente de bytes a caracteres.
             int c;
             while ((c = isr.read()) != -1) {
                 System.out.print((char) c);
             }
        }
//        // Código equivalente con buffer:
//        try (InputStream is = url.openStream();
//             InputStreamReader isr = new InputStreamReader(is);
//             BufferedReader br = new BufferedReader(isr)) { // Lo veremos en el siguiente apartado.
//            String line;
//            while ((line = br.readLine()) != null) {
//                System.out.println(line);
//            }
//        }
    }
}

URI/URL

La clase URL tiene constructores desaprobados, se recomienda emplear URI para crear una URL:

    URI uri = new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
    URL url = uri.toURL();
    url.openStream(); // Abreviatura de:
    url.openConnection().getInputStream(); // openConnection() devuelve un objeto de tipo URLConnection.
// Implantación de openStream() en la clase URL:
public final InputStream openStream() throws java.io.IOException {
    return openConnection().getInputStream();
}

Los constructores de URL está desaprobada, se recomienda emplear URI:

    URI uri = new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
    URL url = uri.toURL();

URLConnection

El método openConnection() de URL devuelve un objeto de tipo URLConnection:

    URI uri = new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/");
    URL url = uri.toURL();
    URLConnection urlConnection = url.openConnection();
    urlConnection.getInputStream();

HttpURLConnection

Permite añadir elementos específicos de HTTP, como el tamaño del contenido, o el tipo de archivo:

  URL url = new URI("https://manuais.pages.iessanclemente.net/plantillas/dam/ad/").toURL();
  HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();  // Hereda de URLConnection
  httpConnection.getInputStream();
  httpConnection.setRequestMethod("HEAD");
  long tamanho = httpConnection.getContentLengthLong();
Ejercicio 4. Lectura de URL

Crea un programa que lea el contenido de una URL y lo muestre por pantalla.

Mejore el programa para que pida una URL y la guarde en un archivo en una carpeta seleccionada del disco mediante un JFileChooser.

¿Serías capaz de mostrar el tamaño del contenido de la URL? ¿Y que ponga la extensión adecuada al archivo?

Ayuda: Puedes emplear HttpURLConnection para obtener el tamaño del contenido y para obtener el Content-Type puedes emplear el método getContentType().

Ejercicio 5. Lectura de URL con HttpURLConnection

Amplía el ejercicio anterior para que emplee HttpURLConnection y muestre la información de la cabecera HTTP.

Última actualización: 23.09.2025

01.05 Flujos de caracteres

Flujos de caracteres (Character Streams)

  • Los flujos de caracteres leen/escriben datos de texto y tienen nombres de clase que terminan en Reader o Writer.
  • Automáticamente, transforma caracteres Unicode (formato de Java) al conjunto de caracteres local.
  • Todas las clases descienden de Reader y Writer.
  • Hay muchas clases de flujos de carácter, como: FileReader (usa internamente FileInputStream), FileWriter (usa internamente FileOutpuStream). Todos los restantes flujos funcionan de igual modo, sólo difieren en la forma de construirlos.

Java almacena valores de caracteres utilizando convenciones Unicode. La E/S de flujos de caracteres traduce automáticamente este formato interno hacia y desde el conjunto de caracteres local. En locales occidentales, como el juego de caracteres Latin-1 o Windows-1252, el conjunto de caracteres local es generalmente un superconjunto de ASCII de 8 bits. En locales asiáticos, el conjunto de caracteres local es un conjunto de caracteres de doble byte.

En la E/S con flujos de caracteres, la entrada y salida realizada con clases de flujo se traduce automáticamente hacia y desde el conjunto de caracteres local. Un programa que utiliza flujos de caracteres en lugar de flujos de bytes se adapta automáticamente al conjunto de caracteres local y está listo para la internacionalización, todo sin esfuerzo adicional por parte del programador.

Si la internacionalización no es prioritario, puedes usar las clases de flujos de caracteres sin prestar mucha atención a los problemas de conjunto de caracteres. Más tarde, si la internacionalización se convierte en una prioridad, tu programa puede adaptarse sin una recodificación extensa (existen constructores y métodos que recogen el juego de caracteres).

1. Reader y Writer

Todas las clases de flujos de caracteres heredan de la clase abstracta Reader y la clase abstracta Writer. Al igual que con los flujos de bytes, existen clases de flujos de caracteres que se especializan en la E/S de archivos: FileReader y FileWriter. El ejemplo CopiarCaracteres ilustra estas clases.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CopiarCaracteres {
    public static void main(String[] args) throws IOException {

        FileReader inputStream = null;
        FileWriter outputStream = null;

        try {
            inputStream = new FileReader("otto.txt");
            outputStream = new FileWriter("nohaycole.txt");

            int c;
            while ((c = inputStream.read()) != -1) {
                outputStream.write(c);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

CopiarCaracteres es muy similar a CopiaArchivos. La diferencia más importante es que CopiarCaracteres ==utiliza FileReader y FileWriter para entrada y salida en lugar de FileInputStream y FileOutputStream==. Observa que tanto CopiaArchivos como CopiarCaracteres emplean una variable int para leer y escribir. Sin embargo, en CopiarCaracteres, la variable int contiene un valor de carácter en sus últimos 16 bits; en CopiaArchivos, la variable int contiene un valor de byte en sus últimos 8 bits.

Con try-with-resources, el código es más limpio y más fácil de leer. FileReader y FileWriter se cierran automáticamente cuando el bloque try-with-resources se completa:

    try (
        FileReader inputStream = new FileReader("otto.txt");
        FileWriter outputStream = new FileWriter("nohaycole.txt");
    ) {
        int c;
        while ((c = inputStream.read()) != -1) {
            outputStream.write(c);
        }
    }

Flujos de caracteres que utilizan flujos de bytes

Los flujos de caracteres suelen ser “envoltorios” para flujos de bytes. El flujo de caracteres utiliza el flujo de bytes para realizar la E/S física, mientras que el flujo de caracteres maneja la traducción entre caracteres y bytes. FileReader, por ejemplo, utiliza FileInputStream, mientras que FileWriter utiliza FileOutputStream.

InputStreamReader y OutputStreamWriter

Son flujos de caracteres que leen y escriben bytes, respectivamente, son flujos de “puente” byte-a-carácter de propósito general: InputStreamReader y OutputStreamWriter.

Se emplean para crear flujos de caracteres cuando no haya clases de flujo de caracteres preempaquetadas que cumplan con las necesidades. Por ejemplo, para crear flujos de caracteres a partir de los flujos de bytes proporcionados por las clases de Socket, como se muestra en el siguiente ejemplo:

import java.io.*;
import java.net.*;

public class ClienteEcho {
    public static void main(String[] args) throws IOException {

        if (args.length != 2) {
            System.err.println("Uso: java ClienteEcho <nombre host> <número puerto>");
            System.exit(1);
        }

        String nombreHost = args[0];
        int numeroPuerto = Integer.parseInt(args[1]);

        try (Socket echoSocket = new Socket(nombreHost, numeroPuerto); // Socket es un flujo de bytes
             PrintWriter out = new PrintWriter(echoSocket.getOutputStream(), true); // PrintWriter es un flujo de caracteres que envía datos a un flujo de bytes. true para autoflush. Escribirá en el flujo de bytes cada vez que se llame a println
             BufferedReader in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream())); // InputStreamReader es un puente byte a carácter, leemos bytes del flujo de bytes y los convertimos a caracteres
             BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)) // InputStreamReader es un puente byte a carácter
        ) {
            String entradaUsuario;
            while ((entradaUsuario = stdIn.readLine()) != null) {
                out.println(entradaUsuario); // envía la entrada del usuario al servidor
                System.out.println("echo: " + in.readLine());
            }
        } catch (UnknownHostException e) {
            System.err.println("Host desconocido " + nombreHost);
            System.exit(1);
        } catch (IOException e) {
            System.err.println("NO ha sido posible establecer la conexión con " +
                    nombreHost);
            System.exit(1);
        }
    }
}

O para lectura desde teclado y salida a consola:

import java.io.*;

public class EjemploPuente {
    public static void main(String[] args) throws IOException {
        try (
            Reader reader = new InputStreamReader(System.in);
            Writer writer = new OutputStreamWriter(System.out);
        ) {
            int c;
            while ((c = reader.read()) != -1) {
                writer.write(c);
            }
        }
    }
}

2. Lectura de líneas completas

La E/S de caracteres suele ocurrir en unidades más grandes que los caracteres individuales. Una unidad común es la línea: una cadena de caracteres con un terminador de línea al final.

Terminadores de línea

Un terminador de línea puede ser una secuencia de retorno de carro/avance de línea ("\r\n"), un solo retorno de carro ("\r"), o un solo avance de línea ("\n"). Admitir todos los terminadores de línea posibles permite a los programas leer archivos de texto creados en cualquiera de los sistemas operativos ampliamente utilizados.

En Windows, el terminador de línea es “\r\n”. En Unix, el terminador de línea es “\n”. En Macintosh, el terminador de línea es “\r”.

Modifiquemos el ejemplo CopiarCaracteres para usar E/S orientada a líneas. Para hacer esto, tenemos que usar dos clases con buffer o memoria internmedia (que guarda los caracteres de toda la línea, o más), BufferedReader y PrintWriter. Veresmos estas clases con mayor profundidad en E/S en el siguiente apartado. El ejemplo CopiarCaracteres invoca BufferedReader.readLine y PrintWriter.println para realizar la entrada y salida una línea a la vez.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class CopiaLineas {
    public static void main(String[] args) throws IOException {

        BufferedReader inputStream = null;
        PrintWriter outputStream = null;

        try {
            inputStream = new BufferedReader(new FileReader("otto.txt"));
            outputStream = new PrintWriter(new FileWriter("nohaycole.txt"));

            String l;
            while ((l = inputStream.readLine()) != null) {
                outputStream.println(l);
            }
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    }
}

Invocar readLine devuelve una línea de texto con la línea. CopiaLineas genera cada línea usando println, que añade el terminador de línea para el sistema operativo actual. Esto puede que no sea el mismo terminador de línea que se usó en el archivo de entrada.

Hay muchas maneras de estructurar la entrada y salida de texto más allá de caracteres y líneas.

3. Diagrama de clases de Reader Java:

Diagrama de clases de Reader Diagrama de clases de Reader


La API a menudo incluye clases similares tanto para flujos de bytes como para flujos de caracteres, como FileInputStream y FileReader. La diferencia entre las dos clases se basa en cómo se leen o escriben los bytes en el flujo.

Es importante recordar que, aunque los flujos de caracteres no contienen la palabra “Stream” en su nombre de clase, siguen siendo flujos de E/S. El uso de “Reader/Writer” en el nombre es simplemente para distinguirlas de los flujos de bytes.

Los flujos de bytes se utilizan principalmente para trabajar con datos binarios, como una imagen o un archivo ejecutable, mientras que los flujos de caracteres se utilizan para trabajar con archivos de texto. Dado que las clases de flujos de bytes pueden escribir todo tipo de datos binarios, incluidas cadenas, se deduce que las clases de flujos de caracteres no son estrictamente necesarias. Sin embargo, existen ventajas en usar las clases de flujos de caracteres, ya que se centran específicamente en la gestión de datos de caracteres y cadenas. Por ejemplo, puedes emplear una clase Writer para escribir un valor de cadena en un archivo sin necesidad de preocuparte por la codificación de caracteres subyacente del archivo.

La codificación de caracteres determina cómo se codifican y almacenan los caracteres en bytes en un flujo y cómo se leen posteriormente o decodifican como caracteres. Aunque esto puede parecer sencillo, Java admite una amplia variedad de codificaciones de caracteres, desde aquellas que pueden utilizar un byte para caracteres latinos, como UTF-8 y ASCII, hasta aquellas que utilizan dos o más bytes por carácter, como UTF-16. No es necesario entrar en detalle sobre las codificaciones de caracteres, pero debes estar familiarizado con sus nombres si te encuentras con ellos algún día y saber por dónde van los tiros ;-).

Flujo de caracteres para texto

En cuanto a la codificación de caracteres, simplemente recuerda que usar un flujo de caracteres es mejor para trabajar con datos de texto que un flujo de bytes. Las clases de flujos de caracteres se crearon por conveniencia, y debes aprovecharlas cuando sea posible.

Última actualización: 23.09.2025

01.06 Flujos de E/S con Buffer


1. Flujos de Bajo Nivel vs. flujos de alto nivel

Otra forma de familiarizarse con la API java.io es dividir los flujos en flujos de bajo nivel y flujos de alto nivel (con buffer o memoria intermedia).

1.1. Flujos de bajo nivel (sin buffer)

Un flujo de bajo nivel (sin buffer) se conecta directamente a la fuente de datos, como un archivo, un array o un String. Los flujos de bajo nivel procesan los datos o recursos en bruto y se acceden de manera directa y sin filtrar.

Por ejemplo, una FileInputStream es una clase que lee datos de archivos de un byte a la vez.

En los flujos sin buffer cada petición de lectura/escritura se envía directamente al sistema E/S: puede ser ineficiente (acceso a disco, actividad de red,…)

1.2. Flujos de alto nivel (con buffer)

Por otro lado, un flujo de alto nivel se construye sobre un flujo mediante el encapsulamiento. La encapsulación es el proceso mediante el cual una instancia se pasa al constructor de otra clase, y las operaciones en la instancia resultante se filtran y aplican a la instancia original.

Por ejemplo, echa un vistazo a los objetos FileReader y BufferedReader en el siguiente código de ejemplo:

try (var br = new BufferedReader(new FileReader("noHayCole.txt"))) {
    System.out.println(br.readLine());
}

En este ejemplo, FileReader es el flujo de bajo nivel para la lectura, mientras que BufferedReader es el flujo de alto nivel que toma un FileReader como entrada. Muchas operaciones en el flujo de alto nivel pasan como operaciones a el flujo de bajo nivel subyacente, como read() o close(). Otras operaciones anulan o agregan nueva funcionalidad a los métodos de el flujo de bajo nivel.

Un flujo de buffer puede agregar nuevos métodos, como readLine(), así como mejoras de rendimiento para leer y filtrar los datos de bajo nivel.

Los flujos de alto nivel pueden tomar otros flujos de alto nivel como entrada. Por ejemplo, aunque el siguiente código pueda parecer un poco extraño al principio, el estilo de encapsular un flujo es bastante común en la práctica:

try (var ois = new ObjectInputStream(new BufferedInputStream(
        new FileInputStream("noHayCole.txt")))) {
    System.out.print(ois.readObject());
}

En este ejemplo, FileInputStream es el flujo de bajo nivel que interactúa directamente con el archivo, la cual está envuelta por BufferedInputStream de alto nivel para mejorar el rendimiento. Finalmente, el objeto completo está envuelto por ObjectInputStream, de alto nivel, que nos permite interpretar los datos como un objeto Java.

Las únicas clases de flujos de bajo nivel con las que debes estar familiarizado son las que operan en archivos. El resto de las clases de flujos no abstractas son todas flujos de alto nivel.

Utiliza flujos con búfer al trabajar con archivos

Como se comentó brevemente, las clases con “Buffered” leen o escriben datos en bloques en lugar de un solo byte o carácter a la vez. La mejora de rendimiento al utilizar una clase con búfer para acceder a un flujo de bajo nivel de archivos no se puede exagerar. A menos que estés haciendo algo muy especializado en tu aplicación, siempre debes envolver un flujo de archivo con una clase con búfer en la práctica.

Una de las razones por las que los flujos con búfer tienden a funcionar tan bien en la práctica es que muchos sistemas de archivos están optimizados para el acceso secuencial al disco. Cuantos más bytes secuenciales leas a la vez, menos viajes de ida y vuelta entre el proceso Java y el sistema de archivos, lo que mejora el acceso de tu aplicación. Por ejemplo, acceder a 1,600 bytes secuenciales es mucho más rápido que acceder a 1,600 bytes dispersos por el disco duro.

2. Clases base para flujos: InputStream, OutputStream, Reader y Writer

La biblioteca java.io define cuatro clases abstractas que son las clases base de todas las clases de flujos definidas en la API:

  • InputStream
  • OutputStream
  • Reader
  • Writer

Frecuentemente, los constructores de flujos de alto nivel toman una referencia de la clase abstracta. Por ejemplo, BufferedWriter toma un objeto Writer como entrada, lo que le permite tomar cualquier subclase de Writer.

Es un error común para iniciados mezclar y combinar clases de flujos que no son compatibles entre sí. Por ejemplo, echa un vistazo a cada uno de los siguientes ejemplos y ve si puedes determinar por qué no se compilan:

new BufferedInputStream(new FileReader("z.txt")); // NO COMPILA por mezclar clases de Reader con clases de InputStream

new BufferedWriter(new FileOutputStream("z.txt")); // NO COMPILA por mezclar clases de Writer con clases de OutputStream

new ObjectInputStream(new FileOutputStream("z.txt")); // NO COMPILA por mezclar clases de InputStream con clases de OutputStream

new BufferedInputStream(new InputStream()); // NO COMPILA porque InputStream es una clase abstracta

Los primeros dos ejemplos no se compilan porque mezclan clases de Reader/Writer con clases de InputStream/OutputStream, respectivamente. El tercer ejemplo no se compila porque estamos mezclando una OutputStream con una InputStream. Aunque es posible leer datos de una InputStream y escribirlos en una OutputStream, envolver el flujo no es la forma de hacerlo.

Como veremos más adelante, los datos deben copiarse, a menudo de manera iterativa. Finalmente, el último ejemplo no se compila porque InputStream es una clase abstracta y, por lo tanto, no puedes crear una instancia de ella.

2.1. Identificación de clases de E/S con flujos

Presta atención al nombre de la clase de E/S, ya que descifrarlo a menudo te proporciona pistas de contexto sobre lo que hace la clase. Por ejemplo, sin necesidad de buscarlo, debería estar claro que FileReader es una clase que lee datos de un archivo como caracteres o cadenas. Además, ObjectOutputStream parece una clase que escribe datos de objeto en un flujo de bytes.

Revisión de las Propiedades de los Nombres de Clase de java.io

  • Una clase con las palabras "InputStream" u “OutputStream” en su nombre se utiliza para leer o escribir datos binarios (o de bytes), respectivamente.

  • Una clase con las palabras "Reader" o “Writer” en su nombre se utiliza para leer o escribir datos de caracteres (o cadenas), respectivamente.

  • La mayoría, pero no todas, las clases de entrada tienen una clase de salida correspondiente (FileInputStream y FileOutputStream, por ejemplo)

  • Un flujo de bajo nivel se conecta directamente a la fuente de datos (FileInputStream y FileOutputStream, por ejemplo):

FileReader in = new FileReader("unaVacaLoca.mp3");
  • Un flujo de buffer se construye sobre otro flujo de bajo mediante encapsulación (dentro de un buffer):
BufferedReader in = new BufferedReader(new FileReader("chocolateCaramelo.mp3"));
  • Una clase con "Buffered" en su nombre lee o escribe datos en grupos de bytes o caracteres de una memoria intermedia o buffer y, a menudo, mejora el rendimiento en sistemas de archivos secuenciales.

Con algunas excepciones, sólo envuelves un flujo con otro flujo si comparten el mismo padre abstracto (FileReader puede ser encapsulado en un BufferedReader, por ejemplo), salvo clases que pasan flujos de bytes (InputStream) en caracteres (Reader), por ejemplo: InputStreamReader

3. Tabla resumen de clases de flujos de E/S

Tabla 1 y Tabla 2 se muestran las clases base abstractas de flujos y las clases concretas de flujos de E/S que debes conocer. Ten en cuenta que la mayoría de la información sobre cada flujo, como si es de entrada o salida o si accede a datos mediante bytes o caracteres, se puede deducir solo por el nombre.

Tabla 1 Las clases base abstractas de flujos de E/S de java.io:

Clase Descripción
InputStream Clase abstracta para todas los flujos de entrada de bytes
OutputStream Clase abstracta para todas los flujos de salida de bytes
Reader Clase abstracta para todas los flujos de entrada de caracteres
Writer Clase abstracta para todas los flujos de salida de caracteres

Tabla 2 Clases implemementadas de flujos de E/S de java.io que debes conocer:

Clase Bajo/Alto Nivel Descripción
FileInputStream Bajo Lee datos de archivos como bytes
FileOutputStream Bajo Escribe datos de archivos como bytes
FileReader Bajo Lee datos de archivos como caracteres
FileWriter Bajo Escribe datos de archivos como caracteres
BufferedInputStream Alto Lee datos de bytes de un flujo de entrada existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedOutputStream Alto Escribe datos de bytes en un flujo de salida existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedReader Alto Lee datos de caracteres de un objeto Reader existente de manera bufferizada, lo que mejora la eficiencia y el rendimiento
BufferedWriter Alto Escribe datos de caracteres en un objeto Writer existente de manera bufferizada, lo que mejora la eficiencia y elrendimiento
ObjectInputStream Alto Deserializa tipos de datos primitivos de Java y gráficos de objetos de Java a partir de un flujo de entrada existente
ObjectOutputStream Alto Serializa tipos de datos primitivos de Java y gráficos de objetos de Java en un flujo de salida existente
PrintStream Alto Escribe representaciones formateadas de objetos Java en un flujo binario
PrintWriter Alto Escribe representaciones formateadas de objetos Java en un flujo de caracteres
Última actualización: 23.09.2025

01.07 Operaciones comunes con flujos de E/S.


1. Operaciones con Flujos de E/S

Aunque existen muchas clases de flujos, muchas de ellas comparten las mismas operaciones. En esta sección, revisaremos los métodos comunes entre varias clases de flujos. En la siguiente sección, cubriremos clases de flujos específicas.

1.1. Lectura y escritura de Datos

Los flujos de E/S se tratan de leer y escribir datos, por lo que no debería sorprendernos que los métodos más importantes sean read() y write(). Tanto InputStream como Reader declaran el siguiente método para leer datos de bytes de un flujo:

// InputStream y Reader

public int read() throws IOException

Del mismo modo, OutputStream y Writer definen el siguiente método para escribir un byte en el flujo:

// OutputStream y Writer

public void write(int b) throws IOException

Espera un momento. Dijimos que estamos leyendo y escribiendo bytes, ¿entonces por qué los métodos usan int en lugar de byte? Recuerda, el tipo de dato byte tiene un rango de 256 caracteres. Se necesitaba un valor adicional para indicar el final de un flujo. Los autores de Java decidieron usar un tipo de dato más grande, int, para que valores especiales como -1 indiquen el final de un flujo. Las clases de flujos de salida también utilizan int para ser coherentes con las clases de flujos de entrada.

// Ejemplo de métodos copyStream() que leen desde un InputStream o Reader
// y escriben en un OutputStream o Writer, respectivamente. En ambos ejemplos,
// -1 se usa para indicar el final del flujo.

void copyStream(InputStream in, OutputStream out) throws IOException {
    int b;

    while ((b = in.read()) != -1) {
        out.write(b);
    }
}

void copyStream(Reader in, Writer out) throws IOException {
    int b;

    while ((b = in.read()) != -1) {
        out.write(b);
    }
}

Las clases de flujos de bytes también incluyen métodos sobrecargados para leer y escribir múltiples bytes a la vez.

// InputStream

public int read(byte[] b) throws IOException

public int read(byte[] b, int offset, int length) throws IOException

// OutputStream

public void write(byte[] b) throws IOException

public void write(byte[] b, int offset, int length) throws IOException

Los valores de offset y length se aplican al array en sí. Por ejemplo, un offset de 5 y una longitud de 3 indican que el flujo debería leer hasta 3 bytes de datos y colocarlos en el array comenzando desde la posición 5.

Existen métodos equivalentes para las clases de flujos de caracteres que usan char en lugar de byte.

// Reader

public int read(char[] c) throws IOException

public int read(char[] c, int offset, int length) throws IOException

// Writer

public void write(char[] c) throws IOException

public void write(char[] c, int offset, int length) throws IOException

1.2. Cierre de flujos

Todos los flujos de E/S incluyen un método para liberar cualquier recurso dentro del flujo cuando ya no se necesita.

// Todas las clases de flujos de E/S

public void close() throws IOException

Dado que los flujos se consideran recursos, es fundamental que todos los flujos de E/S se cierren después de su uso, para evitar posibles fugas de recursos.

Dado que todos los flujos de E/S implementan la interfaz Closeable, la mejor manera de hacerlo es con una declaración try-with-resources.

try (var fis = new FileInputStream("datos.txt")) {
    System.out.print(fis.read());
}

En muchos sistemas de archivos, no cerrar un archivo correctamente podría dejarlo bloqueado por el sistema operativo, impidiendo que otros procesos lo lean o escriban hasta que el programa se termine. EN la medida de lo posible, cerraremos los recursos del flujo usando la sintaxis de try-with-resources, ya que esta es la forma preferida de cerrar recursos en Java. También utilizaremos var para acortar las declaraciones , ya que estas declaraciones pueden volverse bastante largas (en el aula suelo poner el nombre de clase para poner el tipo concreto y que lo conozcáis, pero es mejor hacerlo con var).

¿Y si necesitas pasar un flujo a un método? Eso está bien, pero el flujo debe cerrarse en el método que lo creó.

public void printData(InputStream is) throws IOException {
    int b;

    while ((b = is.read()) != -1) {
        System.out.print(b);
    }
}

public void readFile(String fileName) throws IOException {
    try (var fis = new FileInputStream(fileName)) {
        printData(fis);
    }
}

En este ejemplo, el flujo se crea y se cierra en el método readFile(), mientras que printData() procesa su contenido.

1.3. Cierre de flujos envueltos en otro flujo (con buffer)

Cuando trabajas con un flujo envuelto (con buffer), solo necesitas usar close() en el objeto superior. Al hacerlo, se cerrarán los flujos subyacentes.

El siguiente ejemplo es válido y resultará en tres llamadas separadas a close(), pero es innecesario:

try (var fis = new FileOutputStream("zoo-banner.txt");
      // Innecesario
      var bis = new BufferedOutputStream(fis);
      var ois = new ObjectOutputStream(bis)) {
     ois.writeObject("Hola");
 }

En cambio, podemos confiar en que ObjectOutputStream cierre BufferedOutputStream y FileOutputStream. Lo siguiente llamará solo a un método close() en lugar de tres:

 try (var ois = new ObjectOutputStream(
         new BufferedOutputStream(
             new FileOutputStream("zoo-banner.txt")))) {
     ois.writeObject("Hola");
 }

1.4. Manipulación de flujos de entrada: Mark, Reset y Skip

Todas las clases de flujos de entrada incluyen los siguientes métodos para manipular el orden en el que se leen los datos de un flujo:

// InputStream y Reader

public boolean markSupported();

public void mark(int readLimit);

public void reset() throws IOException;

public long skip(long n) throws IOException;

Los métodos mark() y reset() devuelven un flujo a una posición anterior.

Antes de llamar a cualquiera de estos métodos, debes llamar al método markSupported(), que devuelve true solo si mark() es compatible.

El método skip() es bastante simple; básicamente, lee datos del flujo y descarta el contenido.

mark() y reset()

Supongamos que tenemos una instancia de InputStream cuyos próximos valores son “LEON”. Considera el siguiente fragmento de código:

public void readData(InputStream is) throws IOException {
    System.out.print((char) is.read()); // L
    if (is.markSupported()) {
        is.mark(100); // Marca hasta 100 bytes
        System.out.print((char) is.read()); // E
        System.out.print((char) is.read()); // O
        is.reset(); // Restablece el flujo a la posición antes de E
    }
    System.out.print((char) is.read()); // E
    System.out.print((char) is.read()); // O
    System.out.print((char) is.read()); // N
}

El fragmento de código imprimirá “LEOEON” si mark() es compatible, y “LEON” en caso contrario. Es una buena práctica organizar las operaciones read() de modo que el flujo termine en la misma posición, independientemente de si mark() es compatible o no.

¿Y qué hay del valor 100 que pasamos al método mark()? Este valor se llama readLimit. Le indica al flujo que esperamos llamar a reset() después de leer como máximo 100 bytes. Si el programa llama a reset() después de leer más de 100 bytes al llamar a mark(100), entonces podría lanzar una excepción, dependiendo de la clase de flujo.

skip()

Supongamos que tenemos una instancia de InputStream cuyos próximos valores son “TIGRES”. Considera el siguiente fragmento de código:

System.out.print((char) is.read()); // T
is.skip(2); // Salta I y G
is.read(); // Lee R pero no lo muestra
System.out.print((char) is.read()); // E
System.out.print((char) is.read()); // S

Este código imprimirá “TES” en tiempo de ejecución. Hemos saltado dos caracteres, I y G. También leímos R pero no lo almacenamos en ninguna parte, por lo que se comporta como si hubiéramos llamado a skip(1).

El valor devuelto por skip() nos indica cuántos valores se omitieron realmente . Por ejemplo, si estamos cerca del final del flujo y llamamos a skip(1000), el valor de retorno podría ser 20, lo que indica que se alcanzó el final del flujo después de omitir 20 valores. Usar el valor devuelto por skip() es importante si necesitas llevar un registro de dónde estás en un flujo y cuántos bytes se han procesado.

1.5. Flushing de flujos de salida (Output Streams)

Cuando se escribe datos en un flujo de salida, el sistema operativo subyacente no garantiza que los datos se escriban inmediatamente en el sistema de archivos. En muchos sistemas operativos, los datos pueden almacenarse en la memoria, y la escritura se produce solo después de que se llena una caché temporal o después de un cierto período de tiempo.

Si los datos se almacenan en la memoria y la aplicación termina de manera inesperada, los datos se perderán, ya que nunca se escribieron en el sistema de archivos. Para abordar esto, todas las clases de flujos de salida proporcionan un método flush(), que solicita que todos los datos acumulados se escriban de inmediato en el disco.

// OutputStream y Writer

public void flush() throws IOException

En el siguiente ejemplo, se escriben 1000 caracteres en un flujo de archivo. Las llamadas a flush() aseguran que los datos se envíen al disco duro al menos una vez cada 100 caracteres. La JVM o el sistema operativo son libres de enviar los datos con más frecuencia.

try (var fos = new FileOutputStream(fileName)) {
    for (int i = 0; i < 1000; i++) {
        fos.write('a');
        if (i % 100 == 0) {
            fos.flush();
        }
    }
}

El método flush() ayuda a reducir la cantidad de datos perdidos si la aplicación termina de manera inesperada. Sin embargo, no es gratuito. Cada vez que se usa, puede causar un retraso perceptible en la aplicación, especialmente para archivos grandes. A menos que los datos que estés escribiendo sean extremadamente críticos, el método flush() solo debe usarse de manera intermitente. Por ejemplo, no es necesario llamarlo después de cada escritura.

Tampoco es necesario llamar al método flush() cuando hayas terminado de escribir datos, ya que ==el método close() lo hará automáticamente=0.

2. Resumen de métodos más comunes de flujos de E/S

La Tabla 3 revisa los métodos comunes de flujos que debes conocer para este apartado.

Para los métodos read() y write() que toman arrays primitivos, el tipo de parámetro del método depende del tipo de flujo. Los flujos de bytes que terminan en InputStream/OutputStream utilizan byte[], mientras que los flujos de caracteres que terminan en Reader/Writer utilizan char[].

Tabla 3: Métodos de flujos de E/S más importantes

Flujo Nombre del Método Descripción
Todos los flujos void close() Cierra el flujo y libera los recursos
Todos los flujos de entrada int read() Lee un solo byte o devuelve -1 si no hay bytes disponibles
InputStream int read(byte[] b) Lee valores en un búfer. Devuelve el número de bytes leídos
Reader int read(char[] c) Lee valores en un búfer. Devuelve el número de bytes leídos
InputStream int read(byte[] b, int offset, int length) Lee hasta length valores en un búfer, comenzando desde la posición offset. Devuelve el número de bytes leídos
Reader int read(char[] c, int offset, int length) ee hasta length valores en un búfer, comenzando desde la posición offset. Devuelve el número de bytes leídos
Todos los flujos de salida void write(int) Escribe un solo byte
OutputStream void write (byte[] b) Escribe un array de valores en el flujo
Writer void write(char[] c) Escribe un array de valores en el flujo
OutpuStream void write(byte[] c, int offset, int length) Escribe length valores del array en un flujo, empezando desde el índice offset
Writer void write(char[] c, int offset, int length) Escribe length valores del array en un flujo, empezando desde el índice offset-
Todos los flujos de entrada boolean markSupported() Devuelve true si la clase de flujo admite mark()
Todos los flujos de entrada void mark(int readLimit) Marca la posición actual en el flujo
Todos los flujos de entrada void reset() Intenta restablecer el flujo a la posición marcada
Todos los flujos de entrada long skip(long n) Lee y descarta un número especificado de caracteres
Todos los flujos de salida void flush() Vacía los datos acumulados a través del flujo
Última actualización: 23.09.2025

Ejercicios

Boletín 01. Ejercicos con la clase File Y RandomAccessFile

Recuerda

Para realizar los ejercicios de este boletín, debes crear un nuevo proyecto en tu IDE preferido y añadir las clases que se indican en cada ejercicio. También debes consultar la documentación oficial de la clase File para conocer los métodos que puedes utilizar, así como el apartado: de “Ventanas de entrada de datos, mensajes y archivos” de la unidad de “Refuerzo y ayudas complementarias”, apartado “Java General

Ejercicio 1. Creación y lectura de archivos con File

Debes trabajar únicamente con métodos de la clase File.

Realiza los siguientes pasos:

  1. Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
  2. Escribe un programa que cree un objeto File para el archivo prueba.txt y compruebe si el archivo existe.
  3. Si el archivo existe, muestra la ruta absoluta, nombre del archivo, tamaño, última modificación y si es un directorio.
  4. Si el archivo no existe, muestra un mensaje que lo indique y crea uno temporal.
Ejercicio 2. Mostrar el contenido de un directorio

Debes trabajar únicamente con métodos de la clase File.

El programa abre una ventana para la selección de un directorio (hazlo también desde teclado si recoge un parámetro) y usando el método listFiles() de la clase File, muestra el contenido de ese directorio, indicando el tamaño de los archivos y si es un directorio o no. Además, muestra el tamaño total de los archivos y directorios.

Muestra en una ventana emergente el resultado y por consola.

A continuación puedes ver algunas soluciones parciales del ejercicio 2. Completa el ejercicio de acuerdo a las indicaciones.

Solución parcial con list()
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        File directorio = new File("C:\\Users\\javhoz\\Documents\\GitHub\\dam2\\");
        File[] archivos = directorio.listFiles();
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
        }
    }
}
Solución parcial con JFileChooser
import javax.swing.JFileChooser;
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        fileChooser.showOpenDialog(null);
        File directorio = fileChooser.getSelectedFile();
        File[] archivos = directorio.listFiles();
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
        }
    }
}
Solución completa con JFileChooser
import javax.swing.JFileChooser;
import java.io.File;

public class ListFiles {
    public static void main(String[] args) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        fileChooser.showOpenDialog(null);
        File directorio = fileChooser.getSelectedFile();
        File[] archivos = directorio.listFiles();
        long total = 0;
        for (File archivo : archivos) {
            System.out.println(archivo.getName() + " " + archivo.length() + " " + (archivo.isDirectory() ? "Directorio" : "Archivo"));
            total += archivo.length();
        }
        System.out.println("Tamaño total: " + total);
    }
}
Ejercicio 3. Gestor de archivos y directorios

Como en todos los ejercicios anteriores, debes trabajar únicamente con métodos de la clase File.

Escribe un programa en Java que funcione como un gestor básico de archivos y directorios. El programa debe permitir al usuario realizar las siguientes operaciones:

  1. Crear un directorio, empleando la clase JFileChooser para seleccionar la ruta donde se creará.
  2. Listar todos los archivos y subdirectorios de un directorio de forma recursiva.
  3. Eliminar un archivo o directorio. Si es un directorio, eliminar todo su contenido de forma recursiva.
  4. Mover o renombrar archivos y directorios.

El programa debe ofrecer un menú para que el usuario elija la operación que desea realizar. La selección de directorios o archivos debe realizarse con la clase JFileChooser.

Ejercicio 4. Escritura y lectura de archivos con RandomAccessFile

Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile.

  1. Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
  2. Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
  3. Lee el mensaje y muéstralo por consola.
Ejercicio 5. Escritura y lectura de archivos con RandomAccessFile

Escribe un programa que utilice la clase RandomAccessFile para escribir en un archivo los números del 1 al 10 y luego los lea desde el archivo. Muestra los números leídos en la consola.

Solución al ejercicio 5
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileDemo {
    public static void main(String[] args) {
        try {
            RandomAccessFile raf = new RandomAccessFile("prueba.txt", "rw");
            for (int i = 1; i <= 10; i++) {
                raf.writeInt(i);
            }
            raf.seek(0);
            for (int i = 1; i <= 10; i++) {
                System.out.println(raf.readInt());
            }
            raf.close();
        } catch (IOException e) {
            e.printStackTrace();
    
        }
    }
}
Ejercicio 6. Modificación de Contenido en un Archivo Binario con RandomAccessFile

Escribe un programa en Java que haga lo siguiente:

  • Escriba 10 enteros en un archivo llamado “datos.bin”.
  • Permita al usuario modificar el tercer número almacenado en el archivo por otro número.
  • Muestra los números antes y después de la modificación en la consola.
Solución al ejercicio 6
import java.io.RandomAccessFile;
import java.io.IOException;
import java.util.Scanner;

public class Ejercicio3 {
    public static void main(String[] args) {
        try (RandomAccessFile raf = new RandomAccessFile("datos.bin", "rw")) {
            // Escribir 10 enteros en el archivo
            for (int i = 1; i <= 10; i++) {
                raf.writeInt(i);
            }

            // Leer los números antes de la modificación
            System.out.println("Números antes de la modificación:");
            raf.seek(0);
            for (int i = 0; i < 10; i++) {
                System.out.println(raf.readInt());
            }

            // Solicitar al usuario un nuevo número para el tercer número
            Scanner sc = new Scanner(System.in);
            System.out.print("Introduce un nuevo número para reemplazar el tercer número: ");
            int nuevoNumero = sc.nextInt();

            // Modificar el tercer número (posición 2 en base 0, cada entero ocupa 4 bytes)
            raf.seek(2 * 4);
            raf.writeInt(nuevoNumero);

            // Leer los números después de la modificación
            System.out.println("Números después de la modificación:");
            raf.seek(0);
            for (int i = 0; i < 10; i++) {
                System.out.println(raf.readInt());
            }

        } catch (IOException e) {
            System.out.println("Ocurrió un error de entrada/salida.");
            e.printStackTrace();
        }
    }
}
Ejercicio 7. Escritura y lectura de archivos con RandomAccessFile

Escribe un programa que escriba y lea datos en un archivo usando la clase RandomAccessFile. El programa debe hacer lo siguiente:

  1. Crea un archivo de texto llamado prueba.txt en el directorio actual de tu proyecto, sólo si no existe.
  2. Escribe un programa que cree un objeto RandomAccessFile para el archivo prueba.txt y escriba un mensaje.
  3. Lee el mensaje y muéstralo por consola.

Boletín 02. Ejercicios con flujos I/O

Ejercicio 1. Copia de archivos I/O

Se debe realizar un programa para copiar archivos. El programa debe recoger el nombre del archivo origen y destino. Se existe debe solicitar confirmación sobrescribir.

Úsese I/O con buffer y métodos estáticos (tenga en cuenta que los archivos pueden ser binarios).

a) Para la lectura desde teclado puede emplearse la clase Scanner.

b) Realiza el mismo ejercicio, pero empleando entradas desde ventana con JFileChooser`` y mensajes de error en JOptionPane, si los hay.

c) Realiza un programa que lea con un JOptionPane pida una URL y para posteriormente abrir un JFileChooser para guardarlo en el disco local.

Ayuda: para abrir un flujo de entrada a una URL puede hacerse con el método openStream() de URL. Ten en cuenta que puede lanzar excepciones:

InputStream in = new URL(FILE_URL).openStream();

d) Mejora el aparado a) para que la lectura de los datos lo haga en bloques (buffer) y no byte a byte.

Ejercicio 2. Serialización

Crea una clase Persona con los atributos nombre y edad. Genera un programa que serialice y deserialice un objeto de tipo Persona.

Debe tener un menú con las siguientes opciones:

  1. Añadir persona.
  2. Mostrar personas.
  3. Buscar persona (por número o por nombre, según consideres)
  4. Salir

Puedes hacerlo desde consola o por medio de una interfaz gráfica, haciendo uso de JOptionPane para introducir los datos (JOptionPane.showInputDialog) y mostrar los resultados (JOptionPane.showMessageDialog).

Ejercicio 3. Serialización de colecciones

Crea una clase ColeccionPersonas que contenga una colección de objetos de tipo Persona. Implementa la interface Serializable y crea un programa que serialice y deserialice un objeto de tipo ColeccionPersonas.

Ejercicio 4. Lectura de URL

Crea un programa que lea el contenido de una URL y lo muestre por pantalla.

Mejore el programa para que pida una URL y la guarde en un archivo en una carpeta seleccionada del disco mediante un JFileChooser.

¿Serías capaz de mostrar el tamaño del contenido de la URL? ¿Y que ponga la extensión adecuada al archivo?

Ayuda: Puedes emplear HttpURLConnection para obtener el tamaño del contenido y para obtener el Content-Type puedes emplear el método getContentType().

Ejercicio 5. Lectura de URL con HttpURLConnection

Amplía el ejercicio anterior para que emplee HttpURLConnection y muestre la información de la cabecera HTTP.

Ejercicio 6. Estadísticas de un archivo

Realice un programa que recoja el nombre de un fichero y muestre una estadística de la ruta, número de líneas, número de espacios, número de letras, fecha última modificación, longitud del fichero, … Defina una clase EstatisticaFile con atributos: letras, linhas, espacios, archivo (tipo File).

private File arquivo;
private int linhas;
private int letras;
private int espazos;

Métodos para obtener cada uno de los atributos, existe(), ultimaModificacion(), getRuta(). El constructor recoge el nombre del archivo.

Ejercicio 7. Estadísticas de un archivo con RandomAccessFile

Realice un programa que recoja el nombre de un fichero y muestre una estadística de la ruta, número de líneas, número de espacios, número de letras, fecha última modificación, longitud del fichero, …

Defina una clase EstatisticaFile con atributos: letras, linhas, espacios, archivo (tipo File).

private File arquivo;
private int linhas;
private int letras;
private int espazos;

Métodos para obtener cada uno de los atributos, existe(), ultimaModificacion(), getRuta(). El constructor recoge el nombre del archivo.

Ejercicio 8. Editor de texto

Haz un programa que recoja el nombre de un fichero y muestre su contenido si existe o cree un nuevo en el que puedas escribir si no existe. Ejemplo: java Editor proba.txt

Para tal fin, además del programa, Editor.java, crea la clase Documento con las siguientes características:

  1. Propiedades: arquivo (de tipo File)
  2. Constructores: recoge el nombre del archivo y crea el objeto archivo. Otro que recoja un Objeto de tipo File.
  3. Métodos:
  4. exists(): devuelve verdadero cuando el fichero no es nulo y existe.
  5. readFile(): devuelve una cadena con el contenido del archivo, si existe, obviamente. Emplea StringBuilder.
  6. readFileNIO(): igual al anterior, pero empleado Path y el método readString de Files.
  7. writeFromString(…): recoge una cadena y la escribe en fichero, al final, empleando BufferedWriter.
  8. writeFromStringPrintWriter(…): recoge una cadena y la escribe al final, empleando PrintWriter.
  9. writeFromInputStream(): rue recoge un flujo de tipo InputStream (para, por ejemplo, System.in) y escribe lo recogido por el flujo en el fichero.
  10. writeFromKeyword(): escribe en el archivo lo que se escriba en el teclado.
  11. . getFile(): devuelve el objeto archivo.
  12. toString(): devuelve la ruta absoluta/canónica al archivo.

AppEditor.java recoge el nombre por línea de órdenes. Si existe, muestra el contenido (llama al método readFile()) si no existe pide que introduzcas por teclado. Para acabar de introducir datos debe escribir una línea que sólo contiene un “.”.

Ejercicio 9. Lectura de teclado

Realiza una clase de utilidad Teclado con métodos y atributos estáticos para leer desde teclado, que tenga un atributo estático privado LECTOR de tipo BufferedReader (lector de caracteres con buffer que permite leer línea la línea). La clase debe ter los siguientes métodos: lerString, lerChar, lerInt, lerLong, lerBoolean, lerFloat, lerDouble, lerByte, lerShort, para cada tipo de dato básico. Haz un pequeño programa que haga uso esta clase.

Ayuda: emplead el atributo estático System.in (de tipo java.io.InputStream), así como la clase correspondiente que permita pasar un flujo de tipo Byte a un flujo de tipo Carácter.

Como sabéis, Java ya incorpora clases para facilitar la lectura desde teclado: java.io.Console (java 1.6 y sup.) e java.util.Scanner (java 1.5 e sup.), entre otras, como BufferedReader (java 1.1 y sup.).

Ejercicio 10. Gestión de equipos de fútbol

Haga un programa de gestión de la clasificación de la liga de fútbol. Declare una clase Equipo con los atributos mínimos necesarios: nome, ganhados, perdidos, empatados, golesFavor, golesContra.

Para poder ordenar los equipos debe implantar a interface Comparable, y para poder guardarse con el método writeObject de ObjectOutputStream debe implantar Serializable.

Sobrescribe el método equals para que dos Equipos sean iguales si tienen el mismo nombre (¡¡¡¡implanta hashCode()!!!!)

Los equipos deben guardarse en un fichero “clasificacion.dat”. El programa debe tener un menú con las siguientes opciones: cargar equipos, añadir equipo, guardar equipos, mostrar clasificación, modificar equipo.

Una vez cargados emplee un objeto de tipo TreeSet para que los ordene correctamente.

Ejercicio 11. Gestión de equipos de baloncesto

Haga un programa para la gestión y clasificación de la liga de baloncesto. La clasificación de los equipos se guarda en un archivo llamado clasificacion.dat.

a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica.

Tenga en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.

Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.

Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. No debe existir un constructor por defecto (en la práctica sí si debería tener).

Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, para poder guardar los objetos (writeObject de ObjectOutputStream) y/o recuperarlos (readObject de ObjectInputStream) debe implantar la interface Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interface Serializable.

Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.

b) Declare una clase Clasificacion, con un atributo equipos de tipo ArrayList de Equipo, aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee. Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)

Crea los métodos estáticos: **loadClasificacion**, que cargue la clasificación del archivo y la devuelva, y el método **saveClasificacion**, que guarde la clasificación en el archivo.

Una vez cargados se podría emplear un objeto de tipo TreeSet para que ordene correctamente la clasificación (lo veremos en unidades posteriores)

c) El programa debe tener un menú con las siguientes opciones: a. añadir equipo (pide el nombre del equipo y los valores de los atributos no derivados, añadiendo el equipo a la clasificación) b. mostrar clasificación (muestra la clasificación ordenada de los equipos que están cargados en memoria) c. guardar clasificación (que guarda la clasificación en el archivo clasificacion.dat) d. cargar clasificación (que carga la clasificación del archivo clasificacion.dat) e. salir (sale del programa, debiendo preguntar antes).

Utilice la clase Scanner para leer de teclado.
Ejercicio 12. Lectura de un archivo BMP

Modificación de un archivo BMP.

  1. Haga un programa que lea la cabecera de un archivo BMP sin compresión de 24 bits (un archivo de 24 bits implica que cada pixel se representa con 3 bytes, uno para cada color RGB) y muestre la información de la cabecera. Emplee un flujo de tipo DataInputStream.

La clase DataInputStream permite leer datos primitivos de un flujo de entrada en un formato de datos binarios. Cada método de esta clase lee un dato primitivo de un flujo de entrada en un formato de datos binarios adecuado y devuelve el valor correspondiente.

Para leer los bytes de la cabecera, emplee el método readByte() de la clase DataInputStream o puedes leer todos los bytes con el método readFully(byte[] b).

Puedes emplear el método toBinaryString de la clase Integer para mostrar los bytes en binario.

Defina una clase Cabecera que recoja el nombre del archivo y tenga los atributos necesarios para guardar la información del mismo.

/*
 * 2 signature, must be 4D42 hex
 * 4 size of BMP file in bytes (unreliable)
 * 2 reserved, must be zero
 * 2 reserved, must be zero
 * 4 offset to start of image data in bytes
 * 4 size of BITMAPINFOHEADER structure, must be 40
 * 4 image width in pixels
 * 4 image height in pixels
 * 2 number of planes in the image, must be 1
 * 2 number of bits per pixel (1, 4, 8, or 24)
 * 4 compression type (0=none, 1=RLE-8, 2=RLE-4)
 * 4 size of image data in bytes (including padding)
 * 4 horizontal resolution in pixels per meter (unreliable)
 * 4 vertical resolution in pixels per meter (unreliable)
 * 4 number of colors in image, or zero
 * 4 number of important colors, or zero
 */

Ayuda: para pasar el array de 4 bytes a un entero, puede emplear el método ByteBuffer.wrap(byte[]).order(ByteOrder.LITTLE_ENDIAN).getInt(). En el caso de dos bytes puedes emplear ByteBuffer.wrap(byte[]).order(ByteOrder.LITTLE_ENDIAN).getShort().

También puedes hacer uso del siguiente método, que trabaja a más bajo nivel:

public static int byteAInt(byte[] bytes) {
	return ((bytes[3] & 0xFF) << 24) | ((bytes[2] & 0xFF) << 16) | ((bytes[1] & 0xFF) << 8) | (bytes[0] & 0xFF);
}

O, para short:

public static int byteAInt(byte[] bytes) {
	return ((bytes[1] & 0xFF) << 8) | (bytes[0] & 0xFF);
}

La máscara es necesaria porque Java no tiene tipos sin signo y al hacer el cast a int, los bytes se convierten a enteros con signo. Por ejemplo: si el byte es 0xFF (255), al convertirlo a entero, se convierte en -1. El operador desplazamiento a la izquierda (<<) desplaza los bits a la izquierda y rellena con ceros a la derecha. Si el tipo de dato es byte, se convierte a int antes de hacer la operación.

  1. Diseña e implanta de un programa que lea la cabecera de un BMP y permita invertir la imagen, pasarla a escala de grises, añadir ruido, aclarar y oscurecer. La imagen está a continuación de la cabecera. Para pasar la escala de grises hay que establecer los 3 colores del píxel al mismo nivel con la media de los colores.
Solución parcial lectura archivo BMP
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class LeerCabeceraBMP {
	public static void main(String[] args) {
		try (DataInputStream dis = new DataInputStream(new FileInputStream("imagen.bmp"))) {
			byte[] cabecera = new byte[54];
			dis.readFully(cabecera); // Guardamos la cabecera en un array de bytes
			System.out.println("Cabecera BMP:");
			System.out.println("Signature: " + new String(cabecera, 0, 2)); // La sinatura BMP es 4D42, de 2 bits
			System.out.println("Size: " + byteAInt(cabecera, 2)); // Convierte los 4 bytes a un entero en formato LITTLE_ENDIAN
			System.out.println("Offset: " + byteAInt(cabecera, 10)); // Offset a los datos de la imagen
			System.out.println("Width: " + byteAInt(cabecera, 18)); // Ancho de la imagen
			System.out.println("Height: " + byteAInt(cabecera, 22)); // Alto de la imagen
			System.out.println("Bits per pixel: " + byteAInt(cabecera, 28)); // Bits por pixel
		} catch (IOException e) {
			System.out.println("Error de entrada/salida: " + e.getMessage());
		}
	}

	public static int byteAInt(byte[] bytes, int offset) {
		return ((bytes[offset + 3] & 0xFF) << 24) | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 1] & 0xFF) << 8) | (bytes[offset] & 0xFF);
	}
}
Solución completa Cabecera archivo BMP
package com.javhoz.ad.e06bmp;

/**
 *
 * @author pepecalo
 */

/*
 * 2 signature, must be 4D42 hex
 * 4 size of BMP file in bytes (unreliable)
 * 2 reserved, must be zero
 * 2 reserved, must be zero
 * 4 offset to start of image data in bytes
 * 4 size of BITMAPINFOHEADER structure, must be 40
 * 4 image width in pixels
 * 4 image height in pixels
 * 2 number of planes in the image, must be 1
 * 2 number of bits per pixel (1, 4, 8, or 24)
 * 4 compression type (0=none, 1=RLE-8, 2=RLE-4)
 * 4 size of image data in bytes (including padding)
 * 4 horizontal resolution in pixels per meter (unreliable)
 * 4 vertical resolution in pixels per meter (unreliable)
 * 4 number of colors in image, or zero
 * 4 number of important colors, or zero
 */

import java.io.*;

public class CabeceraBMP {

    public static final String BARRA = "==========================================";
    public static final String NL = System.lineSeparator();
    public static final int TAMANHO = 54;

    private byte[] cabeceraBytes;

    public CabeceraBMP(String arquivo) {
        this(new File(arquivo));
    }

    public CabeceraBMP(File f) {
        this.cabeceraBytes = new byte[TAMANHO];
        try ( DataInputStream dataInputStream = new DataInputStream(
                new FileInputStream(f));) {
            dataInputStream.readFully(cabeceraBytes);
        } catch (FileNotFoundException ex) {
            System.err.println(ex.getMessage());
        } catch (IOException ex) {
            System.err.println(ex.getMessage());
        }
    }

    public String getSinature() {
        return new String(cabeceraBytes, 0, 2);
    }

    public int getTamanoArquivo() {
        return byteArrayToInt(cabeceraBytes, 2);
    }

    public int getReserva1() {
        return byteArrayToShort(cabeceraBytes, 6);
    }

    public int getReserva2() {
        return byteArrayToShort(cabeceraBytes, 8);
    }

    public int getOffsetImage() {
        return byteArrayToInt(cabeceraBytes, 10);
    }

    public int getInfoHeader() {
        return byteArrayToInt(cabeceraBytes, 14);
    }

    public int getAnchura() {
        return byteArrayToInt(cabeceraBytes, 18);
    }

    public int getAltura() {
        return byteArrayToInt(cabeceraBytes, 22);
    }

    public int getNumeroPlanos() {
        return byteArrayToShort(cabeceraBytes, 26);
    }

    public int getBitsPerPixel() {
        return byteArrayToShort(cabeceraBytes, 28);
    }

    public int getTipoCompresion() {
        return byteArrayToInt(cabeceraBytes, 30);
    }

    public String getTipoCompresionAsString() {
        int tipo = getTipoCompresion();
        switch (tipo) {
            case 0 -> {
                return "Sin compresión";
            }
            case 1 -> {
                return "RLE-8";
            }
            case 2 -> {
                return "RLE-4";
            }
            default ->
                throw new AssertionError();
        }

    }

    public int getTamanoImagen() {
        return byteArrayToInt(cabeceraBytes, 34);
    }

    public int getResolucionHorizontalPorMetro() {
        return byteArrayToInt(cabeceraBytes, 38);
    }

    public int getResolucionVerticalPorMetro() {
        return byteArrayToInt(cabeceraBytes, 42);
    }

    public int getNumeroColores() {
        return byteArrayToInt(cabeceraBytes, 46);
    }

    public int getImportanciaColores() {
        return byteArrayToInt(cabeceraBytes, 50);
    }

    // Método para convertir un array de bytes en un entero de 4 bytes (little endian)
    public static int byteArrayToInt(byte[] bytes, int offset) {
        return ((bytes[offset + 3] & 0xFF) << 24)
                | ((bytes[offset + 2] & 0xFF) << 16)
                | ((bytes[offset + 1] & 0xFF) << 8)
                | (bytes[offset] & 0xFF);
    }

    public static int byteArrayToShort(byte[] bytes, int offset) {
        return ((bytes[offset + 1] & 0xFF) << 8)
                | (bytes[offset] & 0xFF);
    }

    /*



 * 4 horizontal resolution in pixels per meter (unreliable)
 * 4 vertical resolution in pixels per meter (unreliable)
 * 4 number of colors in image, or zero
 * 4 number of important colors, or zero
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Cabecera BMP:\n").append(BARRA)
                .append("Firma: ").append(getSinature()).append(NL)
                .append("Tamaño arquivo: ").append(getTamanoArquivo()).append(NL)
                .append("Reserva 1: ").append(getReserva1()).append(NL)
                .append("Reserva 2: ").append(getReserva2()).append(NL)
                .append("Offset datos imagen: ").append(getOffsetImage()).append(NL)
                .append("BITMAPINFOHEADER: ").append(getInfoHeader()).append(NL)
                .append("Anchura: ").append(getAnchura()).append(" píxeles").append(NL)
                .append("Altura: ").append(getAltura()).append(" píxeles").append(NL)
                .append("Número de planos: ").append(getNumeroPlanos()).append(NL)
                .append("Bits por píxel: ").append(getBitsPerPixel()).append(NL)
                .append("Tipo de compresión: ").append(getTipoCompresionAsString()).append(NL)
                .append("Tamaño de la imagen: ").append(getTamanoImagen()).append(" bytes").append(NL)
                .append("Resolución horizontal: ").append(getResolucionHorizontalPorMetro()).append(NL)
                .append("Resolución vertical: ").append(getResolucionVerticalPorMetro()).append(NL)
                .append("Número de colores: ").append(getNumeroColores()).append(NL)
                .append("Importancia de colores: ").append(getImportanciaColores()).append(NL);
        return sb.toString();
    }

	public static void main(String[] args) {
        // Ruta del archivo BMP a leer
        String archivoBMP = "e:\\putin.bmp"; // 

        CabeceraBMP cabecera = new CabeceraBMP(archivoBMP);
        System.out.println(cabecera);
    }

}
Solución pasar a escala de crises archivo BMP
import java.io.*;

public class EscalaGrisesBMP {

	public static void main(String[] args) {
		try (DataInputStream dis = new DataInputStream(new FileInputStream("e:\\putin.bmp"))) {
			byte[] cabecera = new byte[54];
			dis.readFully(cabecera); // Guardamos la cabecera en un array de bytes
			int ancho = ((cabecera[21] & 0xFF) << 24) | ((cabecera[20] & 0xFF) << 16) | ((cabecera[19] & 0xFF) << 8) | (cabecera[18] & 0xFF);
			int alto = ((cabecera[25] & 0xFF) << 24) | ((cabecera[24] & 0xFF) << 16) | ((cabecera[23] & 0xFF) << 8) | (cabecera[22] & 0xFF);
			int bitsPorPixel = ((cabecera[29] & 0xFF) << 8) | (cabecera[28] & 0xFF);
			int tamanoImagen = ((cabecera[37] & 0xFF) << 24) | ((cabecera[36] & 0xFF) << 16) | ((cabecera[35] & 0xFF) << 8) | (cabecera[34] & 0xFF);
			byte[] imagen = new byte[tamanoImagen];
			dis.readFully(imagen); // Guardamos la imagen en un array de bytes
			byte[] imagenGrises = new byte[tamanoImagen];
			for (int i = 0; i < tamanoImagen; i += 3) {
				byte promedio = (byte) ((imagen[i] + imagen[i + 1] + imagen[i + 2]) / 3);
				imagenGrises[i] = promedio;
				imagenGrises[i + 1] = promedio;
				imagenGrises[i + 2] = promedio;
			}
			try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("imagen_grises.bmp"))) {
				dos.write(cabecera);
				dos.write(imagenGrises);
			}
		} catch (IOException e) {
			System.out.println("Error de entrada/salida: " + e.getMessage());
		}
	}
}
Solución añadir ruido archivo BMP
import java.io.*;

public class RuidoBMP {

	public static void main(String[] args) {
		try (DataInputStream dis = new DataInputStream(new FileInputStream("e:\\putin.bmp"))) {
			byte[] cabecera = new byte[54];
			dis.readFully(cabecera); // Guardamos la cabecera en un array de bytes
			int ancho = ((cabecera[21] & 0xFF) << 24) | ((cabecera[20] & 0xFF) << 16) | ((cabecera[19] & 0xFF) << 8) | (cabecera[18] & 0xFF);
			int alto = ((cabecera[25] & 0xFF) << 24) | ((cabecera[24] & 0xFF) << 16) | ((cabecera[23] & 0xFF) << 8) | (cabecera[22] & 0xFF);
			int bitsPorPixel = ((cabecera[29] & 0xFF) << 8) | (cabecera[28] & 0xFF);
			int tamanoImagen = ((cabecera[37] & 0xFF) << 24) | ((cabecera[36] & 0xFF) << 16) | ((cabecera[35] & 0xFF) << 8) | (cabecera[34] & 0xFF);
			byte[] imagen = new byte[tamanoImagen];
			dis.readFully(imagen); // Guardamos la imagen en un array de bytes
			byte[] imagenRuido = new byte[tamanoImagen];
			for (int i = 0; i < tamanoImagen; i++) {
				imagenRuido[i] = (byte) (imagen[i] + (Math.random() * 255 - 128));
			}
			try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("imagen_ruido.bmp"))) {
				dos.write(cabecera);
				dos.write(imagenRuido);
			}
		} catch (IOException e) {
			System.out.println("Error de entrada/salida: " + e.getMessage());
		}
	}
}

Tarea 01. Clases DAO con acceso a ficheros.

Tarea: Gestión de equipos y clasificaciones

Haga un programa para la gestión y clasificación de las ligas, como la ACB. Las clasificaciones de los equipos se guardan en archivos binarios o de texto, según decidas. Por ejemplo: Liga ACB.dat.

a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica. En una liga de fútbol, por ejemplo, se podría añadir el campo estadio y los puntos a favor serían los goles a favor.

Además, ten en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.

Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.

Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. No debe existir un constructor por defecto (en la práctica sí si debería tener).

Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, para poder guardar los objetos (writeObject de ObjectOutputStream) y/o recuperarlos (readObject de ObjectInputStream) debe implantar la interface Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interface Serializable.

Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.

b) Declare una clase Clasificacion, con los atributos:

  • equipos de tipo Set de Equipo, aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee.

  • competicion de tipo String que recoja el nombre de la competición. Por defecto, la competición debe ser “Liga ACB”.

  • Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)

Los constructores de Clasificación deben crear el conjunto de equipos como tipo TreeSet, para que los ordene automáticamente.

c) Interface DAO<T, K> (Data Access Object) es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a los datos. Con los siguientes métodos:

    T get(K id);
    List<T> getAll();
    boolean save(T obxecto);
    boolean delete(T obx);
    boolean deleteAll();
    boolean deleteById(K id);
    void update(T obx);

e) Crea una clase EquipoFileDAO que implemente la interfaz DAO<Equipo, String>. Debe implantar los métodos de la interface. Esta clase debe tener un atributo final, path, de tipo Path con la ruta completa al archivo de datos.

Si se emplea ObjectOutput/InputStream, podría tener un atributo ObjectOutputStream y ObjectInputStream. Si se emplea BufferedWriter/Reader, debe tener un atributo BufferedWriter y BufferedReader. Sin embargo, podría hacerse en cada uno de los métodos de la clase:

Ejemplo de save con ObjectOutputStream personalizado:

boolean append = Files.exists(path);
try (FileOutputStream fos = new FileOutputStream(path.toFile(), append);
     ObjectOutputStream oos = append ? new EquipoOutputStream(fos) : new ObjectOutputStream(fos)) {
oos.writeObject(obxecto);
//            System.out.println("Equipo gardado: " + obxecto);
} catch (IOException e) {
      System.out.println("Erro de Entrada/Saída");
      return false;
}

En la que la clase EquipoOutputStream es una clase que hereda de ObjectOutputStream y sobrescribe el método writeStreamHeader para que no escriba la cabecera del stream.

public class EquipoOutputStream extends ObjectOutputStream {
	public EquipoOutputStream(OutputStream out) throws IOException {
		super(out);
	}

	@Override
	protected void writeStreamHeader() throws IOException {
		// No escribe la cabecera
	}
}

f) Cree una clase ClasificacionFileDAO que implemente la interfaz DAO<Clasificacion, String>. Debe tener un atributo final con la ruta en la que se guardan los datos de la clasificación: ruta. El nombre del archivo debe ser el nombre de la competición seguido de .dat. Constructor al que se le pasa la ruta, etc. Para facilitar el trabajo. los métodos de la clase ClasificacionFileDAO pueden hacer uso de la clase EquipoFileDAO.

Por ejemplo, el método save de ClasificacionFileDAO podría ser:

public boolean save(Clasificacion clasificacion) {
	EquipoFileDAO equipoFileDAO = new EquipoFileDAO(ruta + clasificacion.getCompeticion() + ".dat");
	clasificacion.getEquipos().forEach(equipoFileDAO::save);
	return true;
}

g) El programa debe tener un menú con las siguientes opciones:

a. Añadir equipo (pide el nombre del equipo y los valores de los atributos no derivados, añadiendo el equipo a la clasificación) b. Mostrar clasificación (muestra la clasificación ordenada de los equipos que están cargados en memoria) c. Guardar clasificación (que guarda la clasificación en el archivo clasificacion.dat) d. Cargar clasificación (que carga la clasificación del archivo clasificacion.dat) e. Salir (sale del programa, debiendo preguntar antes).

Utilice la clase Scanner para leer de teclado.

Como mejora, intenta hacerlo con una aplicación gráfica.

Última actualización: 23.09.2025

01.02 Java NIO.2

UD 01.02. Java NIO.2

En este apartado estudiaremos:

En el apartado anterior presentamos la API java.io y disvimos cutimos cómo utilizarla para interactuar con archivos y flujos. En este apartado, nos centraremos en la API de la versión 2 de java.nio, o NIO.2 de manera resumida, para interactuar con archivos. NIO.2 es un acrónimo que significa la segunda versión de la API de Entrada/Salida No Bloqueante, y a veces se conoce como la “New I/O.”

Mostraremos cómo NIO.2 nos permite hacer mucho más con archivos y directorios que la API original de java.io. También te mostraremos cómo aplicar la API de Streams (ojo, no confundir con “streams” de entrada/salida) para realizar operaciones complejas con archivos y directorios. Concluiremos mostrando las diversas formas en que se pueden leer y escribir atributos de archivos utilizando NIO.2.

Presentando NIO.2

En su núcleo, NIO.2 es una sustitución para la antigua clase java.io.File que estudiamos en el apartado anterior. El objetivo de la API es proporcionar una API más intuitiva y rica en funciones para trabajar con archivos y directorios.

Cuando decimos antigua, nos referimos a que el enfoque preferido para trabajar con archivos y directorios en aplicaciones de software más recientes es utilizar NIO.2 en lugar de java.io.File. Como veremos, NIO.2 proporciona muchas características y mejoras de rendimiento que la clase heredada admitía.

Subsecciones de 01.02 Java NIO.2

02.01. La interface Path


La interface Path

La piedra angular de NIO.2 es la interfaz java.nio.file.Path. Una instancia de Path representa una ruta jerárquica en el sistema de almacenamiento hacia un archivo o directorio. Se puede pensar en un Path como el sustitución de NIO.2 para la clase java.io.File, aunque la forma en que se utiliza es un poco diferente.

Antes de profundizar en eso, hablemos de las similitudes entre estas dos implementaciones. Tanto los objetos java.io.File como Path pueden hacer referencia a una ruta absoluta o relativa dentro del sistema de archivos. Además, ambos pueden hacer referencia a un archivo o un directorio. Como hicimos en el apartado de java.io y continuamos haciendo en éste, tratamos a una instancia que apunta a un directorio como un archivo, ya que se almacena en el sistema de archivos con propiedades similares. Por ejemplo, podemos cambiar el nombre de un archivo o directorio con los mismos métodos en ambas APIs.

Ahora, algo completamente diferente. A diferencia de la clase java.io.File, la interfaz Path da soporte para enlaces simbólicos. Un enlace simbólico es un archivo especial dentro de un sistema de archivos que sirve como una referencia o puntero a otro archivo o directorio. La figura siguiente muestra un enlace simbólico desde /zoo/favorite a /zoo/cats/lion:

Ejemplo de ruta simbólica Ejemplo de ruta simbólica

En imagen anterior, la carpeta lion y sus elementos se pueden acceder directamente o a través del enlace simbólico. Por ejemplo, las siguientes rutas hacen referencia al mismo archivo:

/zoo/cats/lion/Cubs.java

/zoo/favorite/Cubs.java

En general, los enlaces simbólicos son transparentes para el usuario, ya que el sistema operativo se encarga de resolver la referencia al archivo real. Java NIO.2 incluye soporte completo para crear, detectar y navegar enlaces simbólicos dentro del sistema de archivos.

Última actualización: 23.09.2025

02.03. Creación de Path


3. La interface Path. Creación de Paths

1. Creación de Path

Dado que Path es una interfaz, no podemos crear una instancia directamente. ¡Después de todo, las interfaces no tienen constructores! Java proporciona varias clases y métodos que puedes usar para obtener objetos de tipo Path (dos, o casi).

¿Por qué Path es una interface? Cuando se crea un Path, la máquina virtual de de Java devuelve la implementación específica para el sistema de archivos subyacente. Por ejemplo, la ruta no es igual para Linux que para Windows.

En la mayoría de las circunstancias se desea realizar las mismas operaciones con el Path, independientemente del sistema de archivos.

La API de Java proporciona Path como0 una interface usando el patrón de diseño Factory (lo veremos más adelante en el curso), que nos evita escribir código complejo o personalizado para cada sistenma de archivos.

1.1. Creando un Path con Path.of

La forma más simple y directa de obtener un objeto Path es utilizar el método Factory estático definido dentro de la interfaz Path.

// Método Factory de Path
public static Path of(String first, String... more)

Es fácil crear instancias de Path a partir de valores de String:

Path path1 = Path.of("fotos/batman.png");
Path path2 = Path.of("c:\\users\\pepe\\notas.txt");
Path path3 = Path.of("/home/otto");

El primer ejemplo crea una referencia a una ruta relativa en el directorio de trabajo actual. El segundo ejemplo crea una referencia a una ruta de archivo absoluta en un sistema basado en Windows. El tercer ejemplo crea una referencia a una ruta de directorio absoluta en un sistema basado en Linux o Mac.

Rutas absolutas vs. relativas

Determinar si una ruta es relativa o absoluta depende del sistema de archivos. Convenciones:

Si una ruta comienza con una barra inclinada hacia adelante (/), es absoluta, con / como el directorio raíz. Ejemplos: /home/foto.png y /no/../hay/./cole

Si una ruta comienza con una letra de unidad (c:), es absoluta, con la letra de unidad como el directorio raíz. Ejemplos: c:/una/vacaloca.png y d:/tren/../rojo/./verde

De lo contrario, es una ruta relativa. Ejemplos: fotos/violin.png y tren/../rojo/./verde

Recuerda . representa el directorio actual y .. el directorio padre.

El método Path.of() también incluye varargs (argumentos variables) para pasar elementos de ruta adicionales. Los valores se combinarán y se separarán automáticamente por el separador de archivos dependiente del sistema operativo que aprendiste en el aparado anterior.

Path path1 = Path.of("fotos", "batman.png");

Path path2 = Path.of("c:", "users", "pepe", "notas.txt");

Path path3 = Path.of("/", "home", "otto");

Estos ejemplos son simplemente otro modo de escribir los ejemplos anterioresvde Path, utilizando la lista de parámetros de valores String en lugar de un solo valor String. La ventaja de varargs es que es más robusto, ya que inserta el separador de ruta del sistema operativo adecuado por ti (sin tener que poner / o \).

1.2. Creando un Path con Paths.get

El método Path.of() se introdujo en Java 11. Otra forma de obtener una instancia de Path es desde la clase Factory java.nio.file.Paths (empleada para crear objetos). Ten en cuenta la ’s’ al final de la clase Paths para distinguirla de la interfaz Path.

// Método Factory Paths
public static Path get(String first, String... more)

Reescribiendo los ejemplos anteriores:

Path path1 = Paths.get("fotos/batman.png");
Path path2 = Paths.get("c:\\users\\pepe\\notas.txt");
Path path3 = Paths.get("/", "home", "otto");

Paths.get() es más “antiguo”, pero puede usarse tanto Path.of() como Paths.get() de manera totalmente intercambiable.

1.3. Creando un Path de URI: Path.of, Paths.get

Otra forma de construir un Path usando la clase Paths es con un valor de URI. Un identificador uniforme de recursos (URI) es una cadena de caracteres que identifica un recurso (remoto o local). Comienza con un esquema que indica el tipo de recurso, seguido de un valor de ruta. Ejemplos de valores de esquema incluyen file:// para sistemas de archivos locales, y http://, https:// y ftp:// para sistemas de archivos remotos.

La clase java.net.URI se utiliza para crear valores de URI.

// Constructor de URI
public URI(String str) throws URISyntaxException

Java incluye varios métodos Factory para la conversión entre objetos Path y URI, creación de Path y creación de URI.

// De URI a Path, usando el método Factory de Path
public static Path of(URI uri)

// De URI a Path, usando el método Factory de Paths
public static Paths get(URI uri)

// De Path a URI, usando el método de instancia de Path:
public URI toURI()

Los siguientes ejemplos hacen referencia al mismo archivo (ojo no está implantado para http y https, en principio):

URI a = new URI("file://nohaycole.txt");
Path b = Path.of(a); // Creación de una Path a partir de una URL.
Path c = Paths.get(a); // Creacación de un Path a partir de una URL.
URI d = b.toUri(); // Conversión de un Path en una URL.

Algunos de estos ejemplos pueden lanzar una IllegalArgumentException en tiempo de ejecución, ya que algunos sistemas requieren que los URI sean absolutos. La clase URI tiene un método isAbsolute(), aunque se refiere a si el URI tiene un esquema, no a la ubicación del archivo.

1.4. Obteniendo un Path con FileSystem.getPath

Java NIO.2 hace un uso extensivo de la creación de objetos con clases con el patrón Factory. Como ya hermos visto, la clase Paths crea instancias de la interfaz Path.

Del mismo modo, la clase FileSystems crea instancias de la clase abstracta FileSystem.

// Método Factory de FileSystems
public static FileSystem getDefault()

La clase FileSystem incluye métodos para trabajar directamente con el sistema de archivos. De hecho, tanto Paths.get() como Path.of() son en realidad atajos para este método de FileSystem:

// Método de instancia de FileSystem

public Path getPath(String first, String... more)

Reescribamos una vez más nuestros tres ejemplos anteriores para mostrar cómo obtener una instancia de Path “a la antigua”:

Path path1 = FileSystems.getDefault().getPath("fotos/batman.png");
Path path2 = FileSystems.getDefault().getPath("c:\\users\\pepe\\notas.txt");
Path path3 = FileSystems.getDefault().getPath("/home/otto");
Conexión a Sistemas de Archivos Remotos

Conexión a Sistemas de Archivos Remotos

Si bien la mayor parte del tiempo queremos acceso a un objeto Path que esté dentro del sistema de archivos local, la clase FileSystems nos da la *libertad para conectarnos a un sistema de archivos remoto+, de la siguiente manera:

// Método Factory de FileSystems

public static FileSystem getFileSystem(URI uri)

Lo siguiente muestra cómo se puede usar este método:

FileSystem fileSystem = FileSystems.getFileSystem(new URI("http://www.imdb.con"));
Path path = fileSystem.getPath("top250.txt");

Este código es útil cuando necesitamos construir objetos Path con frecuencia para un sistema de archivos remoto. NIO.2 nos permite conectarnos tanto a sistemas de archivos locales como remotos, lo cual es una mejora importante sobre la antigua clase java.io.File.

1.5. Creando un Path a partir de un java.io.File: toPath()

Por último, pero no menos importante, podemos obtener instancias de Path utilizando la antigua clase java.io.File. De hecho, también podemos obtener un objeto java.io.File a partir de una instancia de Path.

// De Path a File, usando el método de instancia de Path:
public default File toFile()

// De File a Path, usando el método de instancia de java.io.File:
public Path toPath()

Estos métodos están disponibles por conveniencia y también para ayudar a facilitar la integración entre las API antiguas y las nuevas. Ejemplos:

File file = new File("wittgenstein.png");
Path path = file.toPath();
File vuetaAFile = path.toFile();

Sin embargo, al trabajar con aplicaciones más actuales, se recomienda el uso de Path de NIO.2, ya que contiene muchas más características.

Resumen de las relaciones entre clases de NIO.2

A estas alturas, deberías darte cuenta de que NIO.2 hace un uso extensivo del patrón Factory, cuyo uso es sencillo pero estudiaremos más adelante. Muchas de tus interacciones con Java NIO.2 requieren dos tipos: una clase o interfaz abstracta y una clase Factory o auxiliar. Siguiente imagen muestra las relaciones entre las clases de NIO.2, así como algunas clases principales de java.io y java.net. Relaciones de clases e interfaces de NIO.2:

relaciones Java NIO.2 relaciones Java NIO.2

Revisa la imagen cuidadosamente. Al trabajar con NIO.2, fíjate si el nombre de la clase es singular o plural. Las clases con nombres en plural incluyen métodos para crear u operar en instancias de clases/interfaces con nombres en singular. Recuerda, un Path también se puede crear a partir de la interfaz Path, utilizando el método estático of().

Incluida en el esquema está la clase java.nio.file.Files, que veremos más adelante en detalle. Se trata de una clase auxiliar o de utilidad que opera principalmente en instancias de Path para leer o modificar archivos y directorios reales.

Última actualización: 23.09.2025

02.04. Operaciones comunes de Java NIO.2

Operaciones comunes de NIO.2

A lo largo de este capítulo, veremos numerosos métodos que deberías conocer de Java NIO.2. Antes de entrar en los detalles de cada método, mostraremos algunas funciones comunes a modo introductorio.

Símbolos para rutas

Las rutas absolutas y relativas pueden contener símbolos de ruta. Un símbolo de ruta es una serie reservada de caracteres que tienen un significado especial dentro de algunos sistemas de archivos. Hay dos símbolos básicos (elementales) de ruta que debes conocer, como se indica en la siguiente tabla:

Símbolos de sistema de archivos

Símbolo Descripción
. Referencia al directorio actual
.. Referencia al directorio padre del directorio actual

Ilustramos el uso de los símbolos de ruta en la siguiente figura:

Ruta relativa mediante puntos Ruta relativa mediante puntos

En la figura anterior, el directorio actual es /fish/shark/hammerhead. En este caso, ../swim.txt se refiere al archivo swim.txt en el directorio padre del directorio actual. De manera similar, ./play.png se refiere a play.png en el directorio actual. Estos símbolos también se pueden combinar para un mayor efecto. Por ejemplo, ../../clownfish se refiere al directorio que está dos directorios arriba del directorio actual.

A veces verás símbolos de ruta que son redundantes o innecesarios. Por ejemplo, la ruta absoluta /fish/shark/hammerhead/.././swim.txt se puede simplificar a /fish/shark/swim.txt. Veremos cómo manejar estas redundancias más adelante en el capítulo cuando cubriremos normalize().

Argumentos Opcionales en métodos de NIO.2

Muchos de los métodos de java NIO.2 incluyen un varargs que toma una lista opcional de valores. En la siguiente tabla se presentan los argumentos con los que deberías estar familiarizado, por lo menos su existencia. Argumentos comunes de los métodos de NIO.2

Tipo de Enum Interfaz Heredada Valor de Enum Detalles
LinkOption CopyOption, OpenOption NOFOLLOW_LINKS No seguir enlaces simbólicos.
StandardCopyOption CopyOption ATOMIC_MOVE Mover archivo como operación atómica del sistema de archivos.
COPY_ATTRIBUTES Copiar atributos existentes al nuevo archivo.
REPLACE_EXISTING Sobrescribir el archivo si ya existe.
StandardOpenOption OpenOption APPEND Si el archivo ya está abierto para escribir, entonces añadir al final.
CREATE Crear un nuevo archivo si no existe.
CREATE_NEW Crear un nuevo archivo solo si no existe, fallar en caso contrario.
READ Abrir para acceso de lectura.
TRUNCATE_EXISTING Si el archivo ya está abierto para escribir, entonces borrar el archivo y añadir al principio.
WRITE Abrir para acceso de escritura.

Con las excepciones de Files.copy() y Files.move() (que cubriremos más adelante), no profundizaremos en estos parámetros varargs cada vez que presentemos un método. Aunque el comportamiento de ellos debería ser directo. Por ejemplo, ¿puedes entender lo que hace la siguiente llamada a Files.exists() con LinkOption en el siguiente fragmento de código?

Path path = Paths.get("schedule.xml");
boolean exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);

El Files.exists() simplemente verifica si un archivo existe. Sin embargo, si el parámetro es un enlace simbólico, entonces el método verifica si el objetivo del enlace simbólico existe en su lugar. Proporcionar LinkOption.NOFOLLOW_LINKS significa que el comportamiento predeterminado será anulado, y el método verificará si el enlace simbólico en sí existe.

Ten en cuenta que algunos de los enums en tabla anterior heredan una interfaz. Eso significa que algunos métodos aceptan una variedad de tipos de enums. Por ejemplo, el método Files.move() toma un CopyOption vararg para que pueda aceptar enums de diferentes tipos.

void copy(Path source, Path target) throws IOException {
    Files.move(source, target, LinkOption.NOFOLLOW_LINKS, StandardCopyOption.ATOMIC_MOVE);
}

Gestión de métodos que lanzan IOException

Muchos de los métodos presentados en este apartado lanzan (pueden lanzar) una IOException. Las causas comunes de que un método lance esta excepción incluyen:

  • Pérdida de comunicación con el sistema de archivos subyacente.
  • El archivo o directorio existe pero no se puede acceder o modificar. El archivo existe pero no se puede sobrescribir.
  • Se requiere el archivo o directorio pero no existe.

En general, los métodos que operan en valores abstractos de Path, como los de la interfaz Path o la clase Paths, a menudo no lanzan ninguna excepción verificada. Por otro lado, los métodos que operan o cambian archivos y directorios, como los de la clase Files, a menudo declaran IOException.

Hay excepciones a esta regla, como veremos. Por ejemplo, el método Files.exists() no declara IOException. Si lanzara una excepción cuando el archivo no existiera, ¡nunca podría devolver false!

Última actualización: 23.09.2025

02.05. Métodos y con Path Java NIO.2

Operaciones con Path

Hemos visto los conceptos básicos de NIO.2. Ahora veremos cómo Java NIO.2 proporciona una gran cantidad de métodos y clases que operan en objetos Path, muchos más de los que estaban disponibles en la API java.io. En esta sección, presentamos los métodos de Path más importantes.

Al igual que los valores de String, entre otros objetos, las instancias de Path son inmutables. En el siguiente ejemplo, la operación Path en la segunda línea se pierde ya que p es inmutable:

Path p = Path.of("ballena");
p.resolve("krill"); // Se pierda, debería guardarse en otro Path.
System.out.println(p); // ballena

Muchos de los métodos disponibles en la interfaz Path transforman de alguna manera el valor del path y devuelven un nuevo objeto Path, permitiendo encadenar los métodos. Demostramos el encadenamiento en el siguiente ejemplo, cuyos detalles discutiremos en esta sección del capítulo:

Path.of("/zoo/../home").getParent().normalize().toAbsolutePath();

Muchos de los fragmentos de código ede esta unidad que hemos visto se pueden ejecutar sin que las rutas a las que hacen referencia realmente existan. La JVM se comunica con el sistema de archivos para determinar los componentes de la ruta o el directorio principal de un archivo, sin requerir que el archivo realmente exista. Como regla general, si el método declara una IOException, entonces normalmente requiere que las rutas en las que opera existan.

Métodos principales

1. Visualizando el Path con toString(), getNameCount() y getName()

La interfaz Path contiene tres métodos para recuperar información básica sobre la representación del path.

public String toString() // Devuelve una cadena con el Path completo.ünico que devuelve cadena.
public int getNameCount()
public Path getName(int index)

El primer método, toString(), devuelve una representación de cadena del path completo. De hecho, es el único método en la interfaz Path que devuelve una cadena. Muchos de los otros métodos en la interfaz Path devuelven instancias de Path.

Los métodos getNameCount() y getName() se usan a menudo en conjunto para recuperar el número de elementos en la ruta y una referencia a cada elemento, respectivamente. Estos dos métodos no incluyen el directorio raíz como parte del path.

Path path = Paths.get("/tierra/hipopotamo/harry.feliz");
System.out.println("El nombre del Path es: " + path);
for(int i=0; i<path.getNameCount(); i++) {
    System.out.println(" Elemento " + i + " es: " + path.getName(i));
}

Aunque esta es una ruta absoluta, el elemento raíz no se incluye en la lista de nombres. Como dijimos, estos métodos no consideran el directorio raíz como parte del path.

var p = Path.of("/");
System.out.print(p.getNameCount()); // 0
System.out.print(p.getName(0)); // IllegalArgumentException

Observa que si intentas llamar a getName() con un índice no válido, lanzará una excepción en tiempo de ejecución.

2. Creando un nuevo Path con subpath()

La interfaz Path incluye un método para seleccionar partes de un path.

public Path subpath(int beginIndex, int endIndex)

Las referencias son inclusivas del beginIndex y exclusivas del endIndex. El método subpath() es similar al método getName() anterior, excepto que subpath() puede devolver múltiples componentes de la ruta, mientras que getName() devuelve solo uno. Ambos devuelven instancias de Path, sin embargo.

El siguiente fragmento de código muestra cómo funciona subpath(). También imprimimos los elementos del Path usando getName() para que puedas ver cómo se usan los índices.

var p = Paths.get("/mamifero/omnivoro/mapache.imagen");
System.out.println("El Path es: " + p);

for (int i = 0; i < p.getNameCount(); i++) {
    System.out.println(" Elemento " + i + " es: " + p.getName(i));
}

System.out.println();

System.out.println("subpath(0,3): " + p.subpath(0, 3));
System.out.println("subpath(1,2): " + p.subpath(1, 2));
System.out.println("subpath(1,3): " + p.subpath(1, 3));

La salida de este fragmento de código es la siguiente:

El Path es: /mamifero/omnivoro/mapache.imagen
 Elemento 0 es: mamifero
 Elemento 1 es: omnivoro
 Elemento 2 es: mapache.imagen

subpath(0,3): mamifero/omnivoro/mapache.imagen
subpath(1,2): omnivoro
subpath(1,3): omnivoro/mapache.imagen

Al igual que getNameCount() y getName(), subpath() se indexa desde 0 y no incluye el root. También como getName(), subpath() arroja una excepción si se proporcionan índices no válidos.

var q = p.subpath(0, 4); // IllegalArgumentException
var x = p.subpath(1, 1); // IllegalArgumentException

El primer ejemplo arroja una excepción en tiempo de ejecución, ya que el valor máximo de índice permitido es 3. El segundo ejemplo arroja una excepción ya que los índices de inicio y fin son iguales, lo que lleva a un valor de ruta vacío.

3. Accediendo a los elementos del Path con getFileName(), getParent() y getRoot()

La interfaz Path contiene numerosos métodos para recuperar elementos específicos de un Path, devueltos como objetos Path por sí mismos.

public Path getFileName()
public Path getParent()
public Path getRoot()

El método getFileName() devuelve el elemento Path del archivo o directorio actual, mientras que getParent() devuelve la ruta completa del directorio contenedor. getParent() devuelve null si se opera en la ruta raíz o en la parte superior de una ruta relativa. El método getRoot() devuelve el elemento raíz del archivo dentro del sistema de archivos, o null si la ruta es relativa.

Considera el siguiente método, que imprime varios elementos de Path:

public void printPathInformation(Path path) {
    System.out.println("Nombre del archivo: " + path.getFileName());
    System.out.println("Raíz es: " + path.getRoot());

    Path currentParent = path;

    while ((currentParent = currentParent.getParent()) != null) {
        System.out.println(" Directorio actual es: " + currentParent);
    }
}

El bucle while en el método printPathInformation() continúa hasta que getParent() devuelve null. Aplicamos este método a las siguientes tres rutas:

printPathInformation(Path.of("zoo"));
printPathInformation(Path.of("/zoo/armadillo/shells.txt"));
printPathInformation(Path.of("./armadillo/../shells.txt"));

Esta aplicación de prueba produce la siguiente salida:

Nombre del archivo: zoo Raíz es: null
Nombre del archivo: shells.txt Raíz es: /
 Directorio actual es: /zoo/armadillo
 Directorio actual es: /zoo
 Directorio actual es: .

Revisando la salida de prueba, puedes ver la diferencia en el comportamiento de getRoot() en rutas absolutas y relativas. Como puedes ver en los primeros y últimos ejemplos, getParent() no atraviesa las rutas relativas fuera del directorio de trabajo actual.

También puedes ver que estos métodos no resuelven los símbolos de ruta y los tratan como una parte distintiva de la ruta. Aunque la mayoría de los métodos en esta parte del capítulo tratarán los símbolos de ruta como parte de la ruta, presentaremos uno próximamente que limpia los símbolos de ruta.

4. Verificando el Tipo de Path con isAbsolute() y toAbsolutePath()

La interfaz Path contiene dos métodos para ayudar con rutas relativas y absolutas:

public boolean isAbsolute()
public Path toAbsolutePath()

El primer método, isAbsolute(), devuelve true si la ruta a la que hace referencia el objeto es absoluta y false si la ruta es relativa. Como hemos estudiado anteriormente en este capítulo, si una ruta es absoluta o relativa a menudo depende del sistema de archivos, aunque adoptamos convenciones comunes para simplificar los ejemplos de código.

El segundo método, toAbsolutePath(), convierte un objeto Path relativo en un objeto Path absoluto uniéndolo al directorio de trabajo actual. Si el objeto Path ya es absoluto, el método simplemente devuelve el objeto Path.

El siguiente fragmento de código muestra el uso de ambos métodos al ejecutarse en un sistema Windows y Linux, respectivamente:

var path1 = Paths.get("C:\\birds\\egret.txt");
System.out.println("¿Path1 es Absoluto? " + path1.isAbsolute());
System.out.println("Path Absoluto1: " + path1.toAbsolutePath());

var path2 = Paths.get("birds/condor.txt");
System.out.println("¿Path2 es Absoluto? " + path2.isAbsolute());
System.out.println("Path Absoluto2 " + path2.toAbsolutePath());

La salida para el fragmento de código en cada sistema respectivo se muestra en la siguiente salida de muestra. Para el segundo ejemplo, supón que el directorio de trabajo actual es /home/work.

¿Path1 es Absoluto? true
Path Absoluto1: C:\birds\egret.txt

¿Path2 es Absoluto? false
Path Absoluto2 /home/work/birds/condor.txt

5. Uniéndo Paths con resolve()

Supongamos que quieres concatenar rutas de manera similar a como concatenamos cadenas. La interfaz Path proporciona dos métodos resolve() para hacer precisamente eso.

public Path resolve(Path other)
public Path resolve(String other)

El primer método toma un parámetro Path, mientras que la versión sobrecargada es una forma abreviada del primero que toma un String (y construye el Path por ti). El objeto sobre el cual se invoca el método resolve() se convierte en la base del nuevo objeto Path, con el argumento de entrada agregado al Path. Veamos qué sucede si aplicamos resolve() a una ruta absoluta y una ruta relativa:

Path path1 = Path.of("/gatos/../pantera");
Path path2 = Path.of("comida");
System.out.println(path1.resolve(path2));

El fragmento de código genera la siguiente salida:

/gatos/../pantera/comida

Al igual que los otros métodos que hemos visto hasta ahora, resolve() no elimina los símbolos de ruta. En este ejemplo, el argumento de entrada al método resolve() era una ruta relativa, pero ¿qué pasa si hubiera sido una ruta absoluta?

Path path3 = Path.of("/pavo/comida");
System.out.println(path3.resolve("/tigre/jaula"));

Dado que el parámetro de entrada path3 es una ruta absoluta, la salida sería la siguiente:

/tigre/jaula

Para el examen, debes tener en cuenta la mezcla de rutas absolutas y relativas con el método resolve(). Si se proporciona una ruta absoluta como entrada al método, entonces ese es el valor que se devuelve. En pocas palabras, no puedes combinar dos rutas absolutas usando resolve().

6. Derivando un Path con relativize()

La interfaz Path incluye un método para construir la ruta relativa de un Path a otro, a menudo usando símbolos de ruta.

public Path relativize(Path other)

¿Qué crees que imprimirán los siguientes ejemplos usando relativize()?

var path1 = Path.of("pez.txt");
var path2 = Path.of("pajaros/amigables.txt");
System.out.println(path1.relativize(path2));
System.out.println(path2.relativize(path1));

Los ejemplos imprimen lo siguiente:

../pajaros/amigables.txt
../../pez.txt

La idea es la siguiente: si te encuentras en una ruta en el sistema de archivos, ¿qué pasos necesitarías seguir para llegar a la otra ruta? Por ejemplo, para llegar a fish.txt desde friendly/birds.txt, necesitas subir dos niveles (el archivo mismo cuenta como un nivel) y luego seleccionar fish.txt.

Si ambos valores de la ruta son relativos, entonces el método relativize() calcula las rutas como si estuvieran en el mismo directorio de trabajo actual. Alternativamente, si ambos valores de la ruta son absolutos, entonces el método calcula la ruta relativa desde una ubicación absoluta hasta otra, independientemente del directorio de trabajo actual. El siguiente ejemplo demuestra esta propiedad al ejecutarse en una computadora con Windows:

Path path3 = Paths.get("E:\\habitat");
Path path4 = Paths.get("E:\\sanctuary\\raven\\poe.txt");
System.out.println(path3.relativize(path4));
System.out.println(path4.relativize(path3));

Este fragmento de código produce la siguiente

salida:

..\sanctuary\raven\poe.txt
..\..\..\habitat

El fragmento de código funciona incluso si no tienes una unidad E: en tu sistema. Recuerda, la mayoría de los métodos definidos en la interfaz Path no requieren que la ruta exista.

El método relativize() requiere que ambas rutas sean absolutas o ambas relativas y arroja una excepción si los tipos están mezclados.

Path path1 = Paths.get("/primates/chimpanzee");
Path path2 = Paths.get("bananas.txt");
path1.relativize(path2); // IllegalArgumentException

En sistemas basados en Windows, también requiere que si se utilizan rutas absolutas, entonces ambas rutas deben tener el mismo directorio raíz o letra de unidad. Por ejemplo, lo siguiente también arrojaría una IllegalArgumentException en un sistema basado en Windows:

Path path3 = Paths.get("c:\\primates\\chimpanzee");
Path path4 = Paths.get("d:\\storage\\bananas.txt");
path3.relativize(path4); // IllegalArgumentException

7. Limpiando una Ruta con normalize()

Hasta ahora, hemos presentado varios ejemplos que incluyen símbolos de ruta innecesarios. Afortunadamente, Java proporciona un método para eliminar redundancias innecesarias en una ruta.

public Path normalize()

Recuerda, el símbolo de ruta .. se refiere al directorio padre, mientras que el símbolo de ruta . se refiere al directorio actual. Podemos aplicar normalize() a algunas de nuestras rutas anteriores.

var p1 = Path.of("./armadillo/../shells.txt");
System.out.println(p1.normalize()); // shells.txt

var p2 = Path.of("/gatos/../pantera/comida");
System.out.println(p2.normalize()); // /pantera/comida

var p3 = Path.of("../../pez.txt");
System.out.println(p3.normalize()); // ../../pez.txt

Los dos primeros ejemplos aplican los símbolos de ruta para eliminar las redundancias, pero ¿y el último? Esa es tan simplificada como puede ser. El método normalize() no elimina todos los símbolos de ruta; solo aquellos que se pueden reducir.

El método normalize() también nos permite comparar rutas equivalentes. Considera el siguiente ejemplo:

var p1 = Paths.get("/pony/../weather.txt");
var p2 = Paths.get("/weather.txt");
System.out.println(p1.equals(p2)); // false
System.out.println(p1.normalize().equals(p2.normalize())); // true

El método equals() devuelve true si dos rutas representan el mismo valor. En la primera comparación, los valores de las rutas son diferentes. En la segunda comparación, los valores de las rutas se han reducido a la misma ruta normalizada, /weather.txt. Esta es la función principal del método normalize(), permitirnos comparar mejor diferentes rutas.

8. Recuperando la Ruta del Sistema de Archivos con toRealPath()

Si bien trabajar con rutas teóricas es útil, a veces quieres verificar que la ruta realmente existe dentro del sistema de archivos.

public Path toRealPath(LinkOption... options) throws IOException

Este método es similar a normalize(), en el sentido de que elimina cualquier símbolo de ruta redundante. También es similar a toAbsolutePath(), en el sentido de que unirá la ruta con el directorio de trabajo actual si la ruta es relativa.

Sin embargo, a diferencia de esos dos métodos, toRealPath() arrojará una excepción si la ruta no existe. Además, seguirá enlaces simbólicos, con un parámetro varargs opcional para ignorarlos.

Supongamos que tenemos un sistema de archivos en el que tenemos un enlace simbólico desde /cebra a /caballo. ¿Qué crees que imprimirá lo siguiente, dado un directorio de trabajo actual de /caballo/horario?

System.out.println(Paths.get("/cebra/comida.txt").toRealPath());
System.out.println(Paths.get(".././comida.txt").toRealPath());

La salida de ambas líneas es la siguiente:

/caballo/comida.txt
/caballo/comida.txt

En este ejemplo, tanto las rutas absolutas como las relativas resuelven al mismo archivo absoluto, ya que el enlace simbólico apunta a un archivo real dentro del sistema de archivos.

También podemos usar el método toRealPath() para acceder al directorio de trabajo actual como un objeto Path.

System.out.println(Paths.get(".").toRealPath());

Resumen de los métodos de Path

A modo de resumen, muostramos los métodos de Path que deberías, al menos, haber probado:

Métodos de Path Métodos de Path
Path of(String, String…) Path getParent()
URI toURI() Path getRoot()
File toFile() boolean isAbsolute()
String toString() Path toAbsolutePath()
int getNameCount() Path relativize()
Path getName(int) Path resolve(Path)
Path subpath(int, int) Path normalize()
Path getFileName() Path toRealPath(LinkOption…)

Salvo el método estático Path.of(), todos los métodos en mostrados son métodos de instancia que se pueden llamar en cualquier instancia de Path. Además, solo toRealPath() declara una IOException.

Última actualización: 23.09.2025

02.06. Programación funcional con Java NIO.2


1. Métodos útiles de Files que devuelven Stream

La programación funcional de Java NIO.2 realizar operaciones de archivo extremadamente poderosas, a menudo con sólo unas pocas líneas de código.

La clase Files incluye algunos métodos muy útiles de la API Stream que operan en archivos, directorios y árboles de directorio: find,lines, list, walk.

    public static Stream<Path> find(Path start, int maxDepth,
            BiPredicate<Path, BasicFileAttributes> matcher,
                FileVisitOption... options) throws IOException;

    public static Stream<String> lines(Path path)
            throws IOException;

    public static Stream<String> lines(Path path,
            Charset cs) throws IOException;
    
    public static Stream<Path> list(Path dir)
                         throws IOException

    public static Stream<Path> walk(Path start,
            FileVisitOption... options)
            throws IOException;

    public static Stream<Path> walk(Path start,
            int maxDepth,
            FileVisitOption... options)
            throws IOException;

2. Files.list: listar contenido de un Directorio

El siguiente método de Files lista el contenido de un directorio por medio del método Files.listFiles:

public static Stream<Path> list(Path dir) throws IOException

El método Files.list() es similar al método listFiles() de java.io.File, excepto que devuelve un Stream<Path> en lugar de un array de File File[]. Además, listFiles es un método de instancia, no estático:

public File[] listFiles()

Dado que los streams utilizan la evaluación “perezosa”, esto significa que el método cargará cada elemento del directorio según sea necesario, en lugar de cargar todo el directorio de una vez.

Por ejemplo, puede imprimir el contenido de un directorio con el siguiente código (se obvia la excepción que debe capturarse):

try (Stream<Path> s = Files.list(Path.of("/home"))) {
    s.forEach(System.out::println);
}

Recuerda que el método forEach de Stream se declara del siguiente modo :

void forEach(Consumer<? super T> action)

En este caso, sería un Consumer<? super Path> por lo que debe implantar un método accept que no devuelve nada y recoge un Path (o super de Path):

try (Stream<Path> s = Files.list(Path.of("e:\\"))) {
    s.forEach(p -> System.out.println("p = " + p));
} catch (IOException ex) {
            
}

Hagamos algo más interesante. Recordad que existe el método Files.copy() y que solo realiza una copia superficial de un directorio. Podemos usar Files.list() para realizar una copia profunda de un directorio en otro.

public void copyPath(Path origen, Path destino) {
    try {
        Files.copy(origen, destino);
        if (Files.isDirectory(origen)) {
            try (Stream<Path> s = Files.list(origen)) {
                s.forEach(p ->
                    copyPath(p, destino.resolve(p.getFileName()))
                ); 
            }
        }
    } catch (IOException e) {
        // Manejar excepción
    }
}

El primer método copia la ruta, ya sea un archivo o un directorio. Si es un directorio, se realiza solo una copia superficial. Luego, verifica si la ruta es un directorio y, si lo es, realiza una copia recursiva de cada uno de sus elementos. ¿Y si el método se encuentra con un enlace simbólico? De momento, la JVM no seguirá enlaces simbólicos al usar este método de stream, pero hay forma de hacerlo.

Ejercicio

Realiza un programa que copie todos los archivos *.java (incluidos subdirectorios) en un directorio destinio.

  1. Si el directorio destino no existe debe crearlo.
  2. Recorre el directorio y si es un directorio invócalo recursivamente.
  3. Filtra de modo que el nombre del archivo termine en .java

3. Cierre del Stream

En los dos últimos ejemplos de código, colocamos los objetos Stream dentro de un bloque try-with-resources.

Deben cerrarse los streams a archivos.

Los métodos basados en streams de NIO.2 abren una conexión al sistema de archivos que debe cerrarse correctamente, o de lo contrario podría producirse una fuga de recursos. Una fuga de recursos dentro del sistema de archivos significa que la ruta puede estar bloqueada para su modificación mucho después de que se haya completado el proceso que la utilizó.

Si asumieras que una operación terminal de un stream cerraría automáticamente los recursos de archivo subyacentes, estarías equivocado. (Hubo mucho debate sobre este comportamiento cuando se presentó por primera vez, pero en resumen, se decidió que los desarrolladores deben cerrar el stream).

En el lado positivo, no todos los streams necesitan cerrarse, sólo aquellos que abren recursos, como los que se encuentran en NIO.2. Por ejemplo, no necesitabas cerrar ninguno de los streams de programación funcional.

Aun así, por comodidad, a veces se omite el cierre de recursos de NIO.2 en los ejemplos que mostramos, pero cuando programas, siempre utiliza declaraciones try-with-resources con estos métodos de NIO.2.

4. Recorrido de un árbol de directorios

El Files.list() es útil, recorre sólo el contenido de un solo directorio.

¿Qué pasa si queremos visitar todas las rutas dentro de un árbol de directorios?

REcordad que el sistema de archivos está organizado de manera jerárquica. Por ejemplo, un directorio puede contener archivos y otros directorios, que a su vez pueden contener otros archivos y directorios. Cada registro en un sistema de archivos tiene exactamente un padre, con la excepción del directorio raíz, que se encuentra en la parte superior de todo.

Un sistema de archivos se visualiza comúnmente como un árbol con un solo nodo raíz, con muchas ramas y hojas, como se muestra en la imagen siguiente. En este modelo, un directorio es una rama o nodo interno, y un archivo es un nodo hoja. Estructura de árbol de archivo y directorio:

Estructura en árbol de archivos y directorios Estructura en árbol de archivos y directorios{ width=“500” style=“display: block; margin: 0 auto” }

Una tarea común en un sistema de archivos es iterar sobre los descendientes de una ruta, ya sea registrando información sobre ellos o, más comúnmente, filtrándolos para un conjunto específico de archivos. Por ejemplo, es posible que desees buscar en una carpeta e imprimir una lista de todos los archivos .java. Además, los sistemas de archivos almacenan los registros de archivos de manera jerárquica. En general, si deseas buscar un archivo, debes comenzar con un directorio principal, leer sus elementos secundarios, luego leer sus hijos, y así sucesivamente.

Recorrer un directorio, también conocido como caminar (walk) por un árbol de directorios, es el proceso por el cual comienzas con un directorio principal e iteras sobre todos sus descendientes hasta que se cumple alguna condición o no hay más elementos sobre los cuales iterar. Por ejemplo, si estamos buscando un solo archivo, podemos finalizar la búsqueda cuando se encuentra el archivo o cuando hemos revisado todos los archivos y no encontramos nada.

La ruta de inicio suele ser un directorio específico; después de todo, sería consumidor de tiempo buscar en todo el sistema de archivos en cada solicitud.

4.01 Búsqueda en profundidad y búsqueda en anchura

Existen dos estrategias comunes asociadas con recorrer un árbol de directorios: una búsqueda en profundidad y una búsqueda en amplitud (estas estragias también se pueden extrapolar a cualquier tipo de árbol).

Una búsqueda en profundidad recorre la estructura desde la raíz hasta una hoja arbitraria y luego navega hacia atrás hacia la raíz, recorriendo completamente los caminos que omitió en el camino.

Profundidad de búsqueda

La profundidad de búsqueda es la distancia desde la raíz hasta el nodo actual. Para evitar búsquedas interminables, Java incluye una profundidad de búsqueda que se utiliza para limitar cuántos niveles (o saltos) desde la raíz se permite que vaya la búsqueda.

Búsqueda en anchura

Alternativamente, una búsqueda en amplitud comienza en la raíz y procesa todos los elementos de cada profundidad particular antes de pasar al siguiente nivel de profundidad.

Los resultados están ordenados por profundidad, con todos los nodos en la profundidad 1 leídos antes de todos los nodos en la profundidad 2, y así sucesivamente. Aunque una búsqueda en anchura tiende a ser equilibrada y predecible, también requiere más memoria ya que debe mantener una lista de nodos visitados.

Los métodos de la API de Streams de NIO.2 utilizan una búsqueda en profundidad con un límite de profundidad, que puede cambiarse opcionalmente.

4.02 “Caminar” por un directorio con walk()

La clase Files incluye dos métodos para recorrer el árbol de directorios utilizando una búsqueda en profundidad.

public static Stream<Path> walk(Path start, FileVisitOption... options) throws IOException
public static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) throws IOException

Al igual que nuestros otros métodos de stream, walk() utiliza la evaluación perezosa (lazy) y evalúa un Path solo cuando llega a él. Esto significa que incluso si el árbol de directorios incluye cientos o miles de archivos, > la memoria requerida para procesar un árbol de directorios es baja.

El primer método walk() se basa en una profundidad máxima predeterminada de Integer.MAX_VALUE, mientras que la versión sobrecargada permite al usuario establecer una profundidad máxima. Esto es útil en casos donde el sistema de archivos puede ser grande y sabemos que la información que estamos buscando está cerca de la raíz.

En lugar de simplemente imprimir el contenido de un árbol de directorios, podemos hacer algo más interesante. El siguiente método getPathSize() recorre un árbol de directorios y devuelve el tamaño total de todos los archivos en el directorio:

private long getSize(Path p) {
    try {
        return Files.size(p);
    } catch (IOException e) {
        // Manejar excepción
    }
    return 0L;
}

public long getPathSize(Path origen) throws IOException {
    try (var s = Files.walk(origen)) {
        return s.parallel()
            .filter(p -> !Files.isDirectory(p))
            .mapToLong(this::getSize)
            .sum();
    }
}

Nota: el método LongStream mapToLong(ToLongFunction<? super T> mapper) recoge una interfaz ToLongFunction que debe implantar el método long applyAsLong(T value) que devuelve un long, en nuestro caso empleamos la función getSize que recoge un Path y devuelve un long con el tamaño.

Se necesita el método auxiliar getSize() porque Files.size() declara IOException, y prefiero no poner un bloque try/catch dentro de una expresión lambda. Podemos imprimir los datos usando el método format():

var tamanho = getPathSize(Path.of("/home/pepe"));
System.out.format("Tamaño total: %.2f megabytes", (tamanho / 1000000.0));

Dependiendo del directorio en el que ejecutes esto, imprimirá algo como esto:

Tamaño total del árbol de directorios: 15.30 megabytes

4.03. Aplicación de un límite de profundidad

Digamos que nuestro árbol de directorios es bastante profundo, así que aplicamos un límite de profundidad cambiando una línea de código en nuestro método getPathSize().

try (var s = Files.walk(origen, 5)) {

Esta versión sobrecargada verifica los archivos sólo dentro de 5 pasos del nodo inicial. Un valor de profundidad de 0 indica la propia ruta actual. Dado que el método calcula valores sólo en archivos, se tendrá que asignar un límite de profundidad de al menos 1 para obtener un resultado distinto de cero cuando se aplica este método a un árbol de directorios.

Muchos de los métodos anteriores de NIO.2 recorren enlaces simbólicos por defecto, con un NOFOLLOW_LINKS utilizado para desactivar este comportamiento. El método walk() se comporta de modo diferente porque no sigue enlaces simbólicos por defecto y requiere que se habilite la opción FOLLOW_LINKS. Podemos alterar método el anterior getPathSize() para habilitar el seguimiento de enlaces simbólicos agregando la opción FileVisitOption:

try (var s = Files.walk(source, FileVisitOption.FOLLOW_LINKS)) {

Al recorrer un árbol de directorios, el programa debe tener cuidado con los enlaces simbólicos si están habilitados. Por ejemplo, si nuestro proceso se encuentra con un enlace simbólico que apunta al directorio raíz del sistema de archivos, ¡entonces se buscarían todos los archivos en el sistema!

Peor aún, un enlace simbólico podría llevar a un ciclo, en el que una ruta se visita repetidamente. Un ciclo es una dependencia circular infinita en la que una entrada en un árbol de directorios apunta a uno de sus directorios ancestrales. Digamos que tenemos un árbol de directorios como se muestra en imagen siguiente, con el enlace simbólico: /usuario/pepe/todos que apunta a /usuario. Podemos observar el sistema de archivos con ciclo:

Ruta con enlaces simbólicos Ruta con enlaces simbólicos{ width=“400” style=“display: block; margin: 0 auto” }

¿Qué sucede si intentamos recorrer este árbol y seguir todos los enlaces simbólicos, comenzando con /usuario/pepe? La siguiente tabla muestra las rutas visitadas después de caminar una profundidad de 3. Para simplificar, caminaremos por el árbol en un orden de búsqueda en amplitud, aunque un ciclo ocurre independientemente de la estrategia de búsqueda utilizada. Caminar un directorio con un ciclo usando búsqueda en amplitud:

Profundidad alcanzada Ruta alcanzada
0 /usuario/pepe
1 /usuario/pepe/fotos
1 /usuario/pepe/todos/usuario
2 /usuario/pepe/fotos/avatar.png
2 /usuario/pepe/fotos/otto.png
2 /usuario/pepe/todos/pepe/usuario/pepe
3 /usuario/pepe/todos/pepe/fotos/usuario/pepe/fotos
3 /usuario/pepe/todos/pepe/fotos/todos/usuario/pepe/todos/usuario

Después de caminar una distancia de 1 desde el inicio, alcanzamos el enlace simbólico /usuario/pepe/todos y volvemos a la parte superior del árbol de directorios /usuario. Eso está bien porque aún no hemos visitado /usuario, ¡así que aún no hay un ciclo! Por desghracia, en la profundidad 2, encontramos un ciclo, pues ya se ha visitado el directorio /usuario/pepe en nuestro primer paso, y ahora nos estamos encontrando con él nuevamente. Si el proceso continúa, estaremos condenados a visitar el directorio una y otra vez.

Excepción FileSystemLoopException cuando se visita más de una vez.

Cuando se usa la opción FOLLOW_LINKS, el método walk() realizará un seguimiento de todas las rutas que ha visitado, lanzando una FileSystemLoopException si una ruta se visita dos veces.

5. Buscar un directorio con find()

En el ejemplo anterior, aplicamos un filtro al objeto Stream<Path> para filtrar los resultados, aunque NIO.2 proporciona un método más conveniente.

public static Stream<Path> find(Path start, int maxDepth, 
    BiPredicate<Path,BasicFileAttributes> matcher, 
        FileVisitOption... options) throws IOException

El método find() se comporta de manera similar al método walk(), excepto que toma un BiPredicate para filtrar los datos. También requiere que se establezca un límite de profundidad. Al igual que walk(), find() también admite la opción FOLLOW_LINK.

Nota: esta interface funcional, @FunctionalInterface public interface BiPredicate<T,U>, dispone un método de comprobación: boolean test(T t, U u), que evalúa un predicado con los dos argumentos recogidos. Devuelve true si los argumentos se ajustan al predicado.

Los dos parámetros del BiPredicate son un objeto Path y un objeto BasicFileAttributes. De esta manera, NIO.2 recupera automáticamente la información básica del archivo, lo que permite escribir expresiones lambda complejas que tienen acceso directo a este objeto (la fecha de creación, modificación o acceso, si es un directorio o un archivo regular, si es un enlace simbólico, su tamaño,…). Por ejemplo:

Path path = Paths.get("/coles");
long tamanhoMin = 1_000;

try (var s = Files.find(path, 10, 
        (p, a) -> a.isRegularFile() && p.toString()
                .endsWith(".java") && a.size() > tamanhoMin)) {
    s.forEach(System.out::println);
}

Este ejemplo busca un árbol de directorios e imprime todos los archivos .java con un tamaño de al menos 1,000 bytes, utilizando un límite de profundidad de 10. Aunque podríamos haber logrado esto usando el método walk() junto con una llamada a readAttributes(), esta implementación es mucho más corta y conveniente. Además, no tenemos que preocuparnos de que los métodos dentro de la expresión lambda lancen una excepción verificada, como en el ejemplo de getPathSize().

6. Leer el contenido de un archivo con lines()

Hemos visto cómo leer el contenido de un archivo con Files.readAllLines(), que devuelve una lista de String, y comentamos que usarlo para leer un archivo muy grande podría resultar en un problema de OutOfMemoryError:

public static List<String> readAllLines(Path path)
                                 throws IOException

NIO.2 resuelve este problema con un método de la API de Stream.

public static Stream<String> lines(Path path) throws IOException

El contenido del archivo se lee y procesa de forma perezosa (lazy), lo que significa que sólo se almacena en memoria una pequeña porción del archivo en un momento dado.

Path path = Paths.get("/baby/shark.tututu");
try (var s = Files.lines(path)) {
    s.forEach(System.out::println);
}

Llevando las cosas un paso más allá, podemos aprovechar otros métodos de stream para un ejemplo más avanzado:

Path path = Paths.get("/papa/shark.tututu");
try (var s = Files.lines(path)) {
    s.filter(f -> f.startsWith("CORO:"))
        .map(f -> f.substring(5))
        .forEach(System.out::println);
}

Este código de muestra busca y muestra del archivo las líneas que comiencen con CORO:, imprimiendo el texto que sigue. Suponiendo que el archivo de entrada sharks.log es el siguiente:

Baby Shark,
CORO:doo-doo, doo-doo, doo-doo
Baby Shark,
CORO:doo-doo, doo-doo, doo-doo
Baby Shark,
CORO:doo-doo, doo-doo, doo-doo
Baby Shark

Entonces, la salida de muestra sería la siguiente:

doo-doo, doo-doo, doo-doo
doo-doo, doo-doo, doo-doo
doo-doo, doo-doo, doo-doo

Como puedes ver, la programación funcional en NIO.2 nos da la capacidad de manipular archivos de maneras complejas, a menudo sólo unas pocas expresiones cortas.

6. Files.readAllLines() vs. Files.lines()

Necesitas conocer la diferencia entre readAllLines() y lines(). Ambos de estos ejemplos se compilan y ejecutan:

Files.readAllLines(Paths.get("papi.txt")).forEach(System.out::println);
Files.lines(Paths.get("nepesaltarin.txt")).forEach(System.out::println);

La primera línea lee todo el archivo en memoria y realiza una operación de impresión sobre el resultado, mientras que la segunda línea procesa perezosamente cada línea e imprime a medida que se lee. La ventaja del segundo fragmento de código es que no requiere que todo el archivo se almacene en memoria en ningún momento.

También debes tener en cuidado cuando se mezclan tipos incompatibles. ¿Ves por qué lo siguiente no compila?

Files.readAllLines(Paths.get("nepesaltarin.txt"))
      .filter(String::isEmpty).forEach(System.out::println);

La respuesta es que el método filter() espera un Predicate, y el método readAllLines() devuelve una List<String>. Los dos tipos no son compatibles, por lo que no se puede utilizar un método en el otro sin alguna forma de conversión.

Ahora bien, una código similar que compila es la siguiente:

Files.lines(Paths.get("nepesaltarin.txt"))
      .filter(String::isEmpty).forEach(System.out::println);

Esto se debe a que lines() devuelve un Stream<String>, y filter() espera un > Predicate<String>. Ambos comparten el mismo tipo genérico, por lo que el código compila sin problemas. Esto es un recordatorio importante de que las lambdas y los métodos de referencia deben coincidir exactamente con la firma del método funcional correspondiente. En este caso, la firma del método funcional es Predicate<String>, que coincide con la firma de filter().

7. Comparación de java.io.File y NIO.2

I/O File Método NIO.2
file.delete() Files.delete(path)
file.exists() Files.exists(path)
file.getAbsolutePath() path.toAbsolutePath()
file.getName() path.getFileName()
file.getParent() path.getParent()
file.isDirectory() Files.isDirectory(path)
file.isFile() Files.isRegularFile(path)
file.lastModified() Files.getLastModifiedTime(path)
file.length() Files.size(path)
file.listFiles() Files.list(path)
file.mkdir() Files.createDirectory(path)
file.mkdirs() Files.createDirectories(path)
file.renameTo(otherFile) Files.move(path,otherPath)

Un gran número de métodos de NIO.2 no están disponibles en java IO, como soporte para enlaces simbólicos, asignacion de atributos del distema, y más. Java NIO.2 es una biblioteca más avanzada y poderosa que la tradicional java.io.File.

Última actualización: 23.09.2025

01.03 JSON en Java


UD 01.03 JSON en Java

JSON (JavaScript Object Notation) es un formato de datos independiente del lenguaje que expresa objetos JSON como listas de propiedades (pares de nombre/valor) fácilmente legibles.

Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.

JSON se utiliza normalmente, entras, para:

Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XMLJSON: The Fat-Free Alternative to XML para obtener más información.

Veremos cuáles son las API JSON que existen en Java (no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.

Subsecciones de 01.03 JSON en Java

01.00. Introducción a JSON

1. ¿Qué es JSON?

JSON significa: JavaScript Object Notation (Notación de Objetos de JavaScript).

Es un formato para estructurar datos. Este formato es utilizado por diferentes aplicaciones web para comunicarse entre sí.

JSON es un formato de intercambio de datos popular entre navegadores y servidores web porque los navegadores pueden analizar JSON en objetos JavaScript de forma nativa.

En el servidor, sin embargo, JSON debe analizarse y generarse mediante las API de JSON.

JSON es un formato de datos independiente del lenguaje que expresa objetos JSON como listas legibles por humanos de propiedades (pares de nombre/valor).

Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.

JSON se utiliza normalmente para la comunicación asincrónica entre el navegador y el servidor a través de AJAX (Ajax).

También se utiliza:

  • En Sistemas de gestión de bases de datos NoSQL como MongoDb y CouchDb.
  • En aplicaciones de sitios web de redes sociales como Twitter, Facebook, LinkedIn y Flickr
  • Incluso con la API de Google Maps.

Podría decirse que es el sustituto del formato de intercambio de datos XML:

  • Es fácil estructurar los datos en comparación con XML.
  • Admite estructuras de datos como arrays y objetos.
  • Los documentos JSON se ejecutan rápidamente en el servidor o en cualquier lenguaje que disponga de biblioteca correspondiente.

La sintaxis de JSON procede de la notación de objetos de JavaScript, pero el formato de JSON es sólo texto. La generación y lectura de JSON existe para muchos lenguajes, que suelen disponer de bibliotecas para hacerlo.

Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XML” (JSON: The Fat-Free Alternative to XML para obtener más información.

Veremos cuales son las API JSON existen en Java (no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.

2. Características

  • Es un formato independiente del lenguaje que se deriva de JavaScript.
  • Es legible y escribible por humanos, ya que es un formato de texto plano utilizando la notación de objetos de JavaScript.
  • Es un formato de intercambio de datos basado en texto y ligero, lo que significa que es más sencillo de leer y escribir en comparación con XML.
  • Aunque se deriva de un subconjunto de JavaScript, es independiente del lenguaje. Por lo tanto, el código para generar y analizar datos JSON se puede escribir en cualquier otro lenguaje de programación, como Java.
  • Transmisión de Datos entre Computadoras: JSON se utiliza para enviar datos entre computadoras y programas.

3. Reglas sintácticas

Los datos están organizados en pares de nombre/valor separados por comas. Utiliza llaves para contener los objetos { } y corchetes [ ] para contener los arrays.

JSON presenta un objeto JSON como una lista delimitada por llaves y separada por comas de propiedades (una coma no aparece después de la última propiedad):

{
  propiedad1,
  propiedad2,
  ...
  propiedadN
}

Para cada propiedad, el nombre se expresa como una cadena que generalmente está entre comillas dobles. La cadena del nombre se sigue por dos puntos, que a su vez es seguido por un valor de un tipo específico. Ejemplos incluyen "nombre": "Otto" y "edad": 4.

JSON admite los siguientes seis tipos, que veremos más adelante:

  • Cadena: una secuencia de cero o más caracteres Unicode. Las cadenas están delimitadas por comillas dobles y admiten una sintaxis de escape con barra invertida.
  • Número: un número decimal (en base 10) que puede contener una parte fraccional y puede usar notación exponencial (E).
  • Booleano: Cualquiera de los valores true o false.
  • Array: una lista ordenada de cero o más valores, cada uno de los cuales puede ser de cualquier tipo. Los arrays utilizan la notación de corchetes cuadrados con elementos separados por comas.
  • Objeto: una colección no ordenada de propiedades donde los nombres (también llamados claves) son cadenas. Dado que los objetos están destinados a representar arrays asociativos, se recomienda, aunque no es obligatorio, que cada clave sea única dentro de un objeto. Los objetos están delimitados por llaves y usan comas para separar cada propiedad. Dentro de cada propiedad, los dos puntos separan la clave de su valor.
  • Nulo: Un valor vacío, utilizando la palabra clave null.

Ejemplo

{
    "Libros": [
        {
            "Nombre": "Árboles",
            "Curso": "Introducción a los árboles",
            "Contenido": ["Árbol Binario", "BST", "Árbol Genérico"]
        },
        {
            "Nombre": "Grafos",
            "Temas": ["BFS", "DFS", "Orden Topológico"]
        }
    ]
}

3.1. Sintaxis JSON y reglas

La sintaxis JSON es un subconjunto de la sintaxis de JavaScript.

La sintaxis JSON se deriva de la sintaxis de la notación de objetos de JavaScript:

  • Los datos están en pares de nombre/valor.
  • Los datos están separados por comas.
  • Las llaves ({}) contienen objetos.
  • Los corchetes ([]) contienen arrays.

3.2. Datos JSON - “clave”: valor

Los datos JSON se escriben como pares de nombre/valor (también conocidos como pares clave/valor).

Un par de nombre/valor consiste en un nombre de campo (entre comillas dobles), seguido de dos puntos y luego un valor.

Ejemplo

"nombre": "Otto"

Los nombres JSON requieren comillas dobles.

3.2. JSON - se evalúa como objetos de JavaScript

El formato JSON es casi idéntico a los objetos de JavaScript.

En JSON, las claves deben ser cadenas, escritas entre comillas dobles.

JSON:

{"nombre": "Otto"}

4. Ventajas de JSON

  • Almacena todos los datos en un array para que la transferencia de datos sea más fácil. Es la mejor opción para compartir datos de cualquier tamaño, incluso audio, video, etc.
  • Su sintaxis es muy pequeña, fácil y liviana, por lo que ejecuta y responde de manera más rápida.
  • Tiene un amplio rango de compatibilidad con el navegador y es compatible con los sistemas operativos. No requiere mucho esfuerzo para hacerlo compatible con todos los navegadores.
  • En el lado del servidor, el análisis es la parte más importante que los desarrolladores desean. Si el análisis es rápido en el lado del servidor, el usuario puede obtener una respuesta rápida, por lo que en este caso, el análisis del lado del servidor de JSON es un punto fuerte en comparación con otros.

5. Desventajas de JSON

  • La principal desventaja es que no hay manejo y gestión de errores. Si hay un pequeño error en el script, no se obtendrán datos estructurados.
  • Se vuelve bastante peligroso cuando se usa con algunos navegadores no autorizados. Como el servicio JSON devuelve un archivo JSON envuelto en una llamada a función que debe ser ejecutada por los navegadores, si los navegadores no están autorizados, tus/los datos pueden ser hackeados.
  • Tiene herramientas con soporte limitado que podemos usar durante el desarrollo.

6. Tipos de datos JSON

JSON (JavaScript Object Notation) es el formato de datos más ampliamente utilizado para el intercambio de datos en la web. JSON es un formato de intercambio de datos basado en texto y completamente independiente del lenguaje. Se basa en un subconjunto del lenguaje de programación JavaScript y es fácil de entender y generar.

6.1. Tipos de datos JSON

En JSON, los valores deben ser uno de los siguientes tipos de datos:

  • Una cadena (string)
  • Un número (number)
  • Un objeto (object)
  • Un array (array)
  • Un booleano (boolean)
  • null

A diferencia, en JavaScript, los valores pueden ser todos los anteriores, además de cualquier otra expresión JavaScript válida, incluyendo:

  • Una función (function)
  • Una fecha (date)
  • undefined

JSON admite principalmente 6 tipos de datos:

String (Cadena)

Las cadenas JSON deben escribirse entre comillas dobles, al igual que en el lenguaje Java o C.

En JSON, los valores de tipo cadena deben escribirse entre comillas dobles:

Ejemplo

{"nome":"Wittgenstein"}

Hay varios caracteres especiales (caracteres de escape) en JSON que se pueden usar en cadenas, como \ (barra invertida), / (barra diagonal), b (retroceso), n (nueva línea), r (retorno de carro), t (tabulación horizontal), etc.

Ejemplo:

   { "poeta":"Sylvia Plath" }
   { "obra":"Ariel\/Sirenita", "género": "Poesía" }

Aquí \/ se utiliza como caracter de escape para / (barra diagonal).

Number (Número)

Se representa en base 10 y no se utilizan formatos octales ni hexadecimales.

Un número decimal firmado que puede contener una parte fraccional y puede usar notación exponencial (E).

JSON no permite NotANumber (como NaN), no hace distinción entre enteros y punto flotante. Además, como he comentado anteriormente JSON no reconoce los formatos octal y hexadecimal. (Aunque JavaScript utiliza un formato de punto flotante de doble precisión para todos los valores numéricos, otros lenguajes que implementan JSON pueden codificar los números de manera diferente).

Ejemplo:

   { "edad": 32 }
   { "calificación": 9.5 }

Boolean (Booleano)

Este tipo de datos puede ser verdadero (true) o falso (false).

Ejemplo:

   { "premioPulitzer": true }

Null (Nulo)

Es simplemente un valor nulo definido.

Ejemplo

   {
     "premioNobel": null,
     "publicaciones": 25
   }

Object (Objeto)

Es un conjunto de pares de nombre o valor insertados entre {} (llaves). Las claves deben ser cadenas y deben ser únicas. Múltiples pares de claves y valores se separan por una coma (,).

Dado que los objetos están destinados a representar arrays asociativos, se recomienda, aunque no es obligatorio, que cada clave sea única dentro de un objeto. Los objetos están delimitados por llaves y usan comas para separar cada propiedad. Dentro de cada propiedad, los dos puntos separan la clave de su valor.

Sintaxis:

{ "clave" : valor, .......}

Ejemplo:

{
  "Poeta": {
    "nombre": "Sylvia Plath",
    "edad": 32,
    "géneroLiterario": "Poesía"
  }
}

Array

Es una colección ordenada de cero o más valores y comienza con [ (corchete izquierdo) y termina con ] (corchete derecho). Los valores del array están separados por , (coma).

Sintaxis:

[ valor, .......]

Ejemplo:

{
  "obras": ["Ariel", "The Bell Jar", "Colossus"]
}
{
  "colección": [
    {"añoPublicacion": 1965},
    {"añoPublicacion": 1971},
    {"añoPublicacion": 1960}
  ]
}

6.2. Archivos JSON

El tipo de archivo para archivos JSON es “.json”. El tipo MIME para texto JSON es “a pplication/json”.

7. Ejemplo completo de documento JSON

{
  "Poetas": [
    {
      "nombrePoeta": "Sylvia Plath",
      "obraDestacada": "Ariel",
      "géneroLiterario": "Poesía"
    },
    {
      "nombrePoeta": "Emily Dickinson",
      "obraDestacada": "The Collected Poems",
      "géneroLiterario": "Poesía"
    },
    {
      "nombrePoeta": "Walt Whitman",
      "obraDestacada": "Leaves of Grass",
      "géneroLiterario": "Poesía"
    }
  ]
}
Ejercicio: Clasificación de la Liga ACB de Baloncesto

Clasificación de la Liga de Baloncesto ACB

Equipo Jugados Victorias Derrotas Favor Contra Diferencia
Real Madrid 4 4 0 374 311 63
Baskonia 4 3 1 346 320 26
Bàsquet Girona 4 3 1 353 333 20
UCAM Murcia 4 3 1 340 322 18
Valencia Basket 4 3 1 346 330 16
Barça 4 3 1 349 335 14
Surne Bilbao Basket 4 3 1 322 310 12
Joventut Badalona 4 3 1 329 319 10
Monbus Obradoiro 4 2 2 320 299 21
BAXI Manresa 4 2 2 350 351 -1
Dreamland Gran Canaria 4 2 2 312 338 -26
Unicaja 4 1 3 335 333 2
Río Breogán 4 1 3 314 328 -14
MoraBanc Andorra 4 1 3 310 329 -19
Lenovo Tenerife 4 1 3 317 353 -36
Casademont Zaragoza 4 1 3 317 354 -37
Coviran Granada 4 0 4 353 382 -29
Zunder Palencia 4 0 4 290 330 -40

Crea un documento JSON llamado clasificación.json con al menos 4 equipos.

Última actualización: 23.09.2025

01.01. JSON con el API JavaScript de Java


1. Ejemplo de JSON con el API de Java (Scripting API)

En teoría, JSON no está en la API estándar de Java. Sin embargo, podremos hacerlo con Java’s Scripting API.

Nota: En 2014, Oracle presentó una Propuesta de Mejora de Java (JEP) para agregar una API de JSON a Java. Aunque “JEP 198: Light-Weight JSON API”, http://openjdk.java.net/jeps/198, se actualizó en 2017, probablemente pasarán varios años antes de que esta API de JSON se convierta en parte de Java.

En el siguiente ejemplo, sólo a modo de muestra, podemos usar JavaScript, pero en un contexto de Java mediante la API de Scripting de Java. (No te preocupes, no será demandado, pero es importante saber que existe). El siguiente código fuente Java permite ejecutar código JavaScript:

import java.io.FileReader;
import java.io.IOException;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import static java.lang.System.*;

public class RunJSScript {
    public static void main(String[] args) {
        if (args.length != 1) {
            err.println("uso: java RunJSScript scriptEnJS");
            return;
        }

        ScriptEngineManager manager = new ScriptEngineManager(); // Inicio el API de Scripting
        ScriptEngine engine = manager.getEngineByName("nashorn");

        try {
            engine.eval(new FileReader(args[0])); // Sí, los flujos con importantes
        } catch (ScriptException se) {
            err.println(se.getMessage());
        } catch (IOException ioe) {
            err.println(ioe.getMessage());
        }
    }
}

Ojo: en versiones actuales de Java quizás debas añadir un motor de JavaScript a tu proyecto Maven, como ECMAScript como el proporcionado por Oracle GraalVM Oracle GraalVM for JDK 21

    <dependency>
        <groupId>org.graalvm.js</groupId>
        <artifactId>js</artifactId>
        <version>23.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.graalvm.js</groupId>
        <artifactId>js-scriptengine</artifactId>
        <version>23.0.1</version>
    </dependency>

El método main anterior verifica primero que se haya especificado exactamente un argumento desde línea de órdenes, que es el nombre de un archivo de script. Si no es así, muestra información de uso y termina el programa. Por ello, debe recoger como argumento un programa/script en JavaScript, por ejemplo:

var poeta = {
    "nombre": "Sylvia",
    "apellidos": "Plath",
    "estaViva": false,
    "edad": 30,
    "direccion": {
        "direccionCalle": "21 2nd Street",
        "ciudad": "New York",
        "estado": "NY",
        "codigoPostal": "10021-3100"
    },
    "telefonos": [
        {
            "tipo": "casa",
            "numero": "212 555-1234"
        },
        {
            "tipo": "oficina",
            "numero": "646 555-4567"
        }
    ],
    "hijos": [],
    "marido": null
};

print(poeta.nombre);
print(poeta.apellidos);
print(poeta.direccion.ciudad);
print(poeta.telefonos[1].numero);

Explicación:

Suponiendo que se indicó un sólo argumento de línea de órdenes, se instancia la clase javax.script.ScriptEngineManager. ScriptEngineManager sirve como punto de entrada en la API de Scripting.

A continuación, se llama al método ScriptEngine getEngineByName(String shortName) del objeto ScriptEngineManager para obtener un motor de script correspondiente al valor deseado de shortName. Java 11 admite el motor de script nashorn (aunque ha sido obsoleto), que devuelve como un objeto cuya clase implementa la interfaz javax.script.ScriptEngine.

ScriptEngine declara varios métodos eval() para evaluar un script. main() invoca el método Object eval(Reader reader) para leer el script desde su objeto java.io.FileReader y (asumiendo que no se arroje java.io.IOException) luego evalúa el script. Este método devuelve cualquier valor de retorno del script, que ignoro. Además, este método arroja javax.script.ScriptException cuando ocurre un error en el script.

Compila:

javac RunJSScript.java

Suponiendo el Script se llama poeta.js, ejecuta la aplicación de la siguiente manera:

java RunJSScript poeta.js

Deberías observar la siguiente salida (junto con un mensaje de advertencia sobre la eliminación planeada de Nashorn en una futura versión de JDK):

Sylvia
Plath
New York
646 555-4567

2. Parser de JSON: JSON.parse()

Un objeto JSON existe como texto independiente del lenguaje. Para convertir el texto en un objeto dependiente del lenguaje, necesitas analizar el texto. JavaScript proporciona un objeto JSON con un método parse() para esta tarea. Pasa el texto a analizar como argumento a parse() y recibe el objeto basado en JavaScript resultante como el valor de retorno de este método. parse() lanza una SyntaxError cuando el texto no se ajusta al formato JSON.

Ejemplo de código JavaScript con parse().

var tarjetaJSON = "{ \"numero\": \"1234567890123456\", " +
    "\"caducidad\": \"20/04\", \"tipo\": " +
    "\"visa\" }";
var tarjeta = JSON.parse(tarjetaJSON);

print(tarjeta.numero);
print(tarjeta.caducidad);
print(tarjeta.tipo);

var tarjetaJSON2 = "{ 'tipo': 'visa' }";
var tarjeta2 = JSON.parse(tarjetaJSON2);

Suponiendo que el Script anterior se encuentra en tarjeta.js, ejecuta la aplicación de la siguiente manera:

java RunJSScript tarjeta.js

Deberías observar la siguiente salida:

1234567890123456
20/04
visa
SyntaxError: JSON no válido: <json>:1:2 Se esperaba , o } pero se encontró '
{ 'type': 'visa' }
^ en <eval> en la línea número 11

El error de sintaxis muestra que no puedes delimitar un nombre con comillas simples (solo las comillas dobles son válidas).

Ejercicio 2: lectura de datos de un archivo JSON con Java Script API

Clasificación de la Liga de Baloncesto ACB

A partir del documento JSON anterior, copia el archivo JSON en un documento JavaScript para proceder a su lectura y que devuelva los datos del Obradoiro, suponiendo que es el primero de la lista.
Emplea el método eval que recoge un objeto de tipo Reader. Usa una clase con buffer creada con la API de Java NIO.2.

Como plantilla, emplea el ejemplo de los apuntes:

var poeta = {
    "nombre": "Sylvia",
    "apellidos": "Plath",
    "estaViva": false,
    "edad": 30,
    "direccion": {
        "direccionCalle": "21 2nd Street",
        "ciudad": "New York",
        "estado": "NY",
        "codigoPostal": "10021-3100"
    },
    "telefonos": [
        {
            "tipo": "casa",
            "numero": "212 555-1234"
        },
        {
            "tipo": "oficina",
            "numero": "646 555-4567"
        }
    ],
    "hijos": [],
    "marido": null
};

print(poeta.nombre);
print(poeta.apellidos);
print(poeta.direccion.ciudad);
print(poeta.telefonos[1].numero);
Última actualización: 23.09.2025

01.02. Bibliotecas JSON para Java


1. Introducción

Como hemos comentado, JSON es la abreviatura de JavaScript Object Notation, un formato de intercambio de datos popular entre navegadores y servidores web porque los navegadores pueden analizar JSON en objetos JavaScript de forma nativa, es un formato de datos independiente del lenguaje que expresa objetos JSON como listas legibles por humanos de propiedades (pares de nombre/valor).

Sin embargo, aunque los navegadores puedan analizarlos mediante JavaScript, en el servidor (y en programación cliente) JSON debe analizarse y generarse mediante las API de JSON. Como se ha comentado anteriormente, JSON se utiliza normalmente para la comunicación asíncrona entre el navegador y el servidor a través de AJAX (Ajax).

Este apartado veremos algunas de las muchas opciones que tiene para Java analizar y generar JSON.

Separadores de línea

Nota: JSON permite que el separador de línea Unicode U+2028 y el separador de párrafo U+2029 aparezcan sin escapar en cadenas entre comillas. Dado que JavaScript no admite esta característica, JSON no es un subconjunto adecuado de JavaScript.

Además, también es ampliamente utilizado en:

  • En Sistemas de gestión de bases de datos NoSQL como MongoDb y CouchDb.
  • En aplicaciones de sitios web de redes sociales como Twitter, Facebook, LinkedIn y Flickr.
  • Incluso con la API de Google Maps.
¿JSON o XML?

Nota: Muchos desarrolladores prefieren JSON sobre XML porque consideran que JSON es menos extenso y más fácil de leer. Consulta “JSON: la alternativa baja en calorías a XML” (JSON: The Fat-Free Alternative to XML para obtener más información.

Trabajar con datos JSON en Java puede ser relativamente sencillo, pero, como casi todo en Java, hay muchas opciones y bibliotecas entre las que podemos elegir.

Algunas de esas bibliotecas JSON son:

Otras:

Veremos algunas API JSON existen de Java para JSON (que no están incluidas en JDK), así como trabajar con archivos JSON en Java en general.

2. APIs de JSON en Java

Cuando se popularizó el formato JSON, Java no tenía una implementación estándar de analizador/generador JSON, javax.json.bind. Por ello han surgido varias implementaciones de API de JSON de código abierto para Java.

Desde entonces, Java ha intentado abordar la API JSON de Java que falta en JSR 353, que no es un estándar oficial (de momento).

La comunidad Java también ha desarrollado varias API Java JSON de código abierto. Las API JSON de Java de código abierto a menudo ofrecen más opciones y flexibilidad en la forma en que puede trabajar con JSON que la API JSR 353. Por lo tanto, las API de código abierto siguen siendo opciones decentes (y mejores).

Algunas de las API Java JSON de código abierto más conocidas son:

  1. GSON
  2. Jackson
  3. JSON-B. Jakarta JSON Binding, especificación JSR 367. API: Module jakarta.json
  4. JSON-P. Jakarta JSON Processing. API: Module jakarta.json.bind.
  5. JSON.org. Una de las primeras.
  6. mJson, descontinuado 2017.
  7. Boon, descontinuado 2016.

Referencias:

Rendimiento: Un ejemplo de rendimiento de las diferentes bibliotecas puede consultarse en el siguiente recurso:

https://github.com/fabienrenaud/java-json-benchmark#users-model

Hasta hace poco Jackson era el ganador, pero en la actualidad GSON es probablemente el más completo y uno de los más rápidos (en las pruebas que he comprobado para pequeños proyectos), seguido de cerca por JSONP/JSONB, Jackson y luego JSON.simple en último lugar (no aparece Boon ni JSON.org en este análisis, ni las implementaciones de JSON-P y JSON-B).

Existen también bibliotecas de alto rendimiento como dsl-json o la de Alibaba (China), rápidas y de alta implantación.

A modo de curiosidad, la siguiente tabla se muestran ejemplos de los resultados porcentuales que he encontrado, pero dicha evaluación probablemente haya quedado en anticuada:

Velocidad de parsing MB/ms Tiempo de parsing
GSON 100% 0%
Jackson 58% 70.87%
JSON.simple 79% 126.58%
JSONP 44% 25.49%

En ella, GSON es un claro ganador, aunque con reservas.

1. GSON

GSON es una API Java JSON de Google, de ahí viene la G en GSON. GSON es razonablemente flexible, hasta hace poco, Jackson era más rápido que GSON. Pero hoy en día el rendimiento de GSON supera muchas alternativas:

https://github.com/google/gson

GSON contiene 3 analizadores Java JSON:

  • La clase Gson que puede analizar objetos JSON en objetos Java personalizados y viceversa, a traves de los métodos fromJSon y toJson, respectivamente.

  • El JsonReader, que es el analizador JSON de flujos de GSON, que analiza un token JSON a la vez.

  • El JsonParser que puede analizar JSON en una estructura de árbol de objetos Java específicos de GSON.

    Lo veremos más en detalle en esta unidad.

Dependencia de Maven:

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.11.0</version>
</dependency>

2. Jackson

Jackson es una API Java JSON que proporciona varias formas diferentes de trabajar con JSON. Jackson es una de las API Java JSON más populares que existen. La página inicial de Jackson es la siguiente:

https://github.com/FasterXML/jackson

Jackson contiene dos analizadores/parsers JSON diferentes:

  • El Jackson ObjectMapper que analiza JSON en objetos Java personalizados, o en una estructura de árbol específica de Jackson (modelo de árbol).
  • El Jackson JsonParser, que es el analizador de extracción JSON de Jackson, analizando JSON un token a la vez.

Jackson también contiene generador JSON:

  • El Jackson JsonGenerator que puede generar JSON un token a la vez.

Ejemplo:

Dependencias maven

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.3</version>
</dependency>

Código:

public void serializaDeserializaJackson() 
  throws IOException{
    // Creación del objeto:
    Alumno objeto = new Alumno(4,"Otto");
    // Mapeador
    ObjectMapper mapper = new ObjectMapper();
    // Conversión en JSON (serialización):
    String jsonStr = mapper.writeValueAsString(objeto); // Cadena JSON
    // Lectura de objeto JSON:
    Alumno alumno = mapper.readValue(jsonStr, Alumno.class); // Deserialización
}

La cadena será algo como (depende de las propieddades de la clase Alumno):

{
    "edad":4,
    "nombre":"Otto"
}

3. JSONP: Jakarta JSON Processing

JSONP es API JSON compatible compatible con JSR 374 significa que si utiliza las API estándar, debería ser posible intercambiar la implementación de JSONP con otra API en el futuro, sin cambiar el código. Puedes encontrar información JSONP en el repositorio y en la página oficial:

4. JSON-P y JSON-B (Java API for JSON Binding)

La especificación JSON-B proporciona una capa de enlace sobre JSON-P, lo que simplifica aún más la conversión de objetos hacia y desde JSON (más sencillo ;-))

4.1. Java API for JSON Processing (JSON-P)

  • Propósito: JSON-P proporciona una API para procesar (analizar y generar) documentos JSON. Está diseñada para ser una solución de bajo nivel y se centra principalmente en proporcionar un modelo de objeto JSON (similar a un árbol) y una forma de navegar y manipular ese modelo.
  • Características:
    • Ofrece dos modelos: Object Model (similar a un árbol) y Streaming API (procesamiento basado en eventos).
    • Se utiliza para analizar documentos JSON en una estructura de objetos Java (JsonObject, JsonArray, etc.).
    • Puede usarse para generar documentos JSON a partir de objetos Java.
    • Forma parte de la especificación Java EE (Enterprise Edition), pero también es aplicable en entornos Java SE (Standard Edition).

4.2. Java API for JSON Binding (JSON-B)

  • Propósito: JSON-B se centra en la serialización y deserialización automática entre objetos Java y JSON. Su objetivo principal es simplificar la tarea de convertir objetos Java en notación JSON y viceversa, eliminando la necesidad de escribir manualmente código de conversión.
  • Características:
    • Define un conjunto de anotaciones (@JsonbProperty, @JsonbTransient, etc.) para personalizar el mapeo entre los objetos Java y JSON.
    • Permite la personalización a través de adaptadores y estrategias.
    • No proporciona un modelo de objeto JSON como JSON-P, ya que su enfoque es más alto nivel, centrado en la conversión entre objetos Java y JSON.
    • Es parte de las especificaciones de Java EE y también está disponible para aplicaciones Java SE.

JSON-P es más general y se utiliza para el procesamiento directo de JSON, mientras que JSON-B se especializa en la serialización y deserialización de objetos Java a y desde JSON.

¿Cuál es mejor?

JSON-B es la API preferida para convertir objetos Java hacia y desde JSON, gracias a su seguridad de tipos, facilidad de uso y comentarios en tiempo de compilación. Sin embargo, en algunos casos, JSON-P podría ser más adecuado.

Ejemplo de JSON-P

Dependencia Maven JSON-P:

<!-- (Más actual) Versión Jakarta: Jakarta JSON Processing defines a Java(R) based framework for parsing, generating, transforming, and querying JSON documents -->
<dependency>
    <groupId>jakarta.json</groupId>
    <artifactId>jakarta.json-api</artifactId>
    <version>2.1.2</version>
</dependency>


<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.json</artifactId>
    <version>2.0.1</version>
</dependency>
<!-- (Más antiguo) -->
<dependency>
    <groupId>javax.json</groupId>
    <artifactId>javax.json-api</artifactId>
    <version>1.1</version>
</dependency>

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.json</artifactId>
    <version>1.1</version>
</dependency>
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonWriter;
import java.io.StringWriter;

public class JsonPExemplo {

    public static void main(String[] args) {
        // Crear un objeto JSON usando JSON-P
        JsonObject objetoJson = Json.createObjectBuilder()
                .add("nombre", "Otto")
                .add("edad", 4)
                .add("ciudad", "Santiado de Compostela")
                .build();

        // Convertir el objeto JSON a una cadena
        StringWriter stringWriter = new StringWriter();
        try (JsonWriter jsonWriter = Json.createWriter(stringWriter)) {
            jsonWriter.writeObject(objetoJson);
        }

        // Imprimir la cadena JSON
        String strJson = stringWriter.toString();
        System.out.println("JSON Resultante (JSON-P):");
        System.out.println(strJson);
    }
}

Ejemplo de JSON-B

Dependencia Maven JSON-B:

Especificación e implementación:

<!-- (Más actual) Versión Jakarta: Jakarta JSON Processing defines a Java(R) based framework for parsing, generating, transforming, and querying JSON documents -->
<dependency>
  <groupId>jakarta.json.bind</groupId>
  <artifactId>jakarta.json.bind-api</artifactId>
  <version>3.0.0</version>
</dependency>


<dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>javax.json.bind</groupId>
    <artifactId>javax.json.bind-api</artifactId>
    <version>1.0</version>
</dependency>
                    
<dependency>
    <groupId>org.eclipse</groupId>
    <artifactId>yasson</artifactId>
    <version>1.0</version>
</dependency>

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.json</artifactId>
    <version>1.1</version>
</dependency>
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;

public class JsonBExemplo {

    public static void main(String[] args) {
        // Crear un objeto Java
        Persona persona = new Persona("Otto", 4, "Santiago de Compostela");

        // Crear un objeto Jsonb
        Jsonb jsonb = JsonbBuilder.create();

        // Convertir el objeto Java a JSON
        String strJson = jsonb.toJson(persona);

        // Imprimir la cadena JSON
        System.out.println("JSON Resultante (JSON-B):");
        System.out.println(strJson);
    }

    // Clase de ejemplo
    static class Persona {
        String nome;
        int idade;
        String cidade;

        public Persona(String nome, int idade, String cidade) {
            this.nome = nome;
            this.idade = idade;
            this.cidade = cidade;
        }
    }
}

En ellos puede verse la creación y conversión de objetos JSON usando JSON-P y JSON-B. Por supuesto,deben añadirse las bibliotecas correspondientes en tu proyecto para ejecutar estos ejemplos, como javax.json-api para JSON-P y javax.json.bind-api y org.eclipse.yasson para JSON-B.

Ejercicio con JSON-B

Crea un proyecto Maven con una sencilla clase Examen que contenga los siguientes atributos:

  • materia: de tipo String.
  • fecha: de tipo LocalDateTime.
  • participantes: de tipo List de String con los nombres de los estudiantes.

Crea los métodos get/set que consideres adecuados, así como un método toString() que devuelva la materia, la fecha seguida de la lista de participantes (emplea StringBuilder).

Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.

Guarda el examen en un archivo JSON llamado accesoADatos.json mediante el api de JSON-B y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.

Ayuda:

4. JSON.org

Logo JSON Logo JSON JSON.org también tiene una API Java JSON de código abierto. Esta fue una de las primeras API Java JSON disponibles. Es razonablemente fácil de usar, pero no tan flexible o rápido como las otras API JSON mencionadas anteriormente.

Puedes encontrar JSON.org en:

https://github.com/douglascrockford/JSON-java

Como también dice el repositorio de Github, ésta es una antigua API Java JSON. No recomiendo su uso a menos que el proyecto ya lo esté usando. De lo contrario, busca una de las otras opciones más actualizadas, preferiblemente GSON o Jackson.

5. mJson (descontinuado)

mJson es una pequeña biblioteca Java para JSON (creada por el desarrollador Borislav Lordanov) que se utiliza para analizar objetos JSON en objetos Java y viceversa. Esta biblioteca está documentada en GitHub (http://bolerio.github.io/mjson/, y presenta las siguientes características:

  • Soporte completo para la validación de JSON Schema Draft 4.
  • Un único tipo universal: todo es un objeto Json; no hay conversión de tipos.
  • Un único método de tipo Factory para convertir un objeto Java en un objeto Json; simplemente llama a Json.make(cualquier objeto Java aquí).
  • Análisis rápido y codificado a mano.
  • Diseñado como una estructura de datos de propósito general para su uso en Java.
  • Punteros de padre y método up() para recorrer la estructura JSON.
  • Métodos concisos para leer (Json.at()), modificar (Json.set(), Json.add()), duplicar (Json.dup()), y fusionar (Json.with()).
  • Fusión flexible de estructuras profundas Deep-merging.
  • Métodos para la verificación de tipos (por ejemplo, Json.isString()) y acceso al valor subyacente de Java (por ejemplo, Json.asString())
  • Encadenamiento de métodos
  • Factory adaptable para construir tu propio soporte para el mapeo arbitrario entre Java y JSON
  • Biblioteca completa ubicada en un archivo Java, sin dependencias externas.

A diferencia de otras bibliotecas JSON, mJson se centra en la manipulación de estructuras JSON en Java sin asignarlas a objetos Java fuertemente tipados. Como resultado, mJson reduce la la escritura de código y permite trabajar con JSON en Java tan sencillo como en JavaScript.

6. Boon (descontinuado)

Boon es una API Java JSON menos conocida, pero supuestamente es (era) la más rápida de todas (según el último benchmark que he podido comprobar). Boon se está utilizando como la API JSON estándar en Groovy.
Repositorio:

https://github.com/boonproject/boon

La API de Boon es muy similar a la de Jackson (por lo que es fácil de cambiar). Pero Boon es más que una API Java JSON. Boon es un kit de herramientas de propósito general para trabajar con datos fácilmente. Esto es útil, por ejemplo, dentro de los servicios REST, aplicaciones de procesamiento de archivos, etc.

Boon contiene los siguientes analizadores Java JSON:

  • El Boon ObjectMapper que puede analizar JSON en objetos personalizados o mapas Java

Al igual que en Jackson, Boon ObjectMapper también se puede utilizar para generar JSON a partir de objetos Java personalizados.

Última actualización: 23.09.2025

01.03. Gson. Introducción


1. Introducción

GSON es el analizador (parser) y generador JSON de Google para Java. Google desarrolló GSON para uso interno, pero lo abrió más tarde. GSON es razonablemente fácil de usar. En este apartado veremos cómo usar GSON para analizar objetos JSON en Java y serializar objetos Java en JSON.

GSON contiene varias clases del API que se pueden usar para trabajar con JSON. En principio, nos centraremos en los componentes de GSON que analiza documentos JSON en en objetos Java o genera JSON a partir de objetos Java:

https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/module-summary.html

GSON contiene 3 analizadores Java JSON:

  • La clase Gson que puede analizar objetos JSON en objetos Java personalizados y viceversa, a traves de los métodos fromJSon y toJson, respectivamente.

  • El GSON JsonReader, que es el analizador JSON de flujos de GSON, que analiza un token JSON a la vez.

  • El GSON JsonParser que puede analizar JSON en una estructura de árbol de objetos Java específicos de GSON.

Para utilizar GSON en la aplicación Java es necesario incluir el archivo GSON JAR en la ruta de clases de su aplicación Java.

También puede hacerse agregando GSON como una dependencia de Maven a su proyecto, o descargando el archivo JAR e incluirlo en la ruta de clase manualmente:

<dependencies>
  <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.11.0</version>
  </dependency>
</dependencies>

Referencia Maven: https://mvnrepository.com/artifact/com.google.code.gson/gson

Diagrama:

Estructura de clases Gon Estructura de clases Gon

Enlaces:

2. Gson: convertir objetos Java a JSON y viceversa

Gson es una biblioteca de Java que se puede utilizar para convertir objetos Java en su representación JSON. También se puede utilizar para convertir una cadena JSON en un objeto Java equivalente.

Gson puede trabajar con objetos Java arbitrarios, incluidos los objetos preexistentes de los que no tiene el código fuente.

Existen algunos proyectos de código abierto que pueden convertir objetos Java a JSON, como los que hemos visto en el apartado anterior. Sin embargo, la mayoría de las apis de JSON requieren que coloque anotaciones de Java en sus clases, algo que no puede hacer si no tiene acceso al código fuente. Además, la mayoría de ellos no admiten completamente el uso de genéricos de Java.

Gson considera ambos como objetivos de diseño muy importantes y no precisa anotaciones y permite genéricos.

3. Características de Gson

  • Proporciona métodos toJson() y fromJson() simples para convertir objetos Java a JSON y viceversa.
  • Permite la conversión de objetos preexistentes y que no se puedan modificar a y desde JSON.
  • Amplio soporte de genéricos de Java.
  • Permite representaciones personalizadas para objetos.
  • Admite objetos arbitrariamente complejos (con jerarquías de herencia profundas y uso extensivo de tipos genéricos).

4. Configuración y descarga

Dependiendo del tipo de proyecto empleado:

Gradle

dependencies {
  implementation 'com.google.code.gson:gson:2.11.0'
}

Maven

<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.11.0</version>
</dependency>

Descarga del archivo JAR de GSON

Si el proyecto Java no emplea Maven, también se puede descargar el archivo JAR GSON directamente desde el repositorio central de Maven:

Una vez descargado el archivo JAR y puede agregarse al classpath de su aplicación Java:

Proceso de descarga de JAR y documentación

Gson se distribuye como un único archivo JAR; gson-2.10.1.jar es el archivo JAR más reciente, ahora. Para conseguir el archivo JAR, puedes ir al repositorio Maven este enlace, clic en el enlace de descargas y selecciona “jar” del menú desplegable, luego guarda el archivo gson-2.10.1.jar cuando se te pida hacerlo. Además, es posible que desees descargar gson-2.10.1-javadoc.jar, que contiene la documentación de esta API.

Nota: Gson tiene licencia según la Licencia Apache Versión 2.0 (www.apache.org/licenses/).

Es fácil trabajar con gson-2.10.1.jar. Simplemente inclúyelo en el CLASSPATH al compilar el código fuente o al ejecutar una aplicación, de la siguiente manera:

javac -cp gson-2.10.1.jar archivo_fuente
java -cp gson-2.10.1.jar;. archivo_clase_principal

5. Prerrequisitos

Versión mínima de Java SE

  • Gson 2.9.0 y posterior: Java 7
  • Gson 2.8.9 y anteriores: Java 6

A pesar de admitir versiones antiguas de Java, Gson también proporciona un descriptor de módulo JPMS (nombre del módulo: com.google.gson) para usuarios de Java 9 o posterior.

Dependencias de JPMS (Java 9+)

Estos son los módulos opcionales del Sistema de Módulos de Plataforma Java (JPMS) en los que Gson depende. Esto sólo se aplica al ejecutar Java 9 o posterior.

  • java.sql (opcional desde Gson 2.8.9): Cuando este módulo está presente, Gson proporciona adaptadores predeterminados para algunas clases de fecha y hora SQL.

  • jdk.unsupported, respectivamente, la clase sun.misc.Unsafe (opcional): Cuando este módulo está presente, Gson puede utilizar la clase Unsafe para crear instancias de clases sin constructor sin argumentos (sin constructor por defecto). Sin embargo, hay que tener cuidado al depender de esto. Unsafe no está disponible en todos los entornos y su uso tiene algunas trampas; consulta GsonBuilder.disableJdkUnsafe().

Nivel mínimo de API de Android

  • Gson 2.11.0 y posterior: API nivel 21
  • Gson 2.10.1 y anteriores: API nivel 19

Es posible que versiones antiguas de Gson también admitan niveles de API más bajos, aunque esto no se ha verificado.

6. Paquetes y clases Gson

Gson está compuesto por más de 30 clases e interfaces distribuidas en cuatro paquetes:

https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/module-summary.html

  • com.google.gson: este paquete proporciona acceso a Gson, la clase principal para trabajar con Gson.
  • com.google.gson.annotations: este paquete proporciona tipos de anotaciones para su uso con Gson.
  • com.google.gson.reflect: este paquete proporciona una clase de utilidad para obtener información de tipo de un tipo genérico.
  • com.google.gson.stream: este paquete proporciona clases de utilidad para leer y escribir valores codificados en JSON.

Empezaremos con la clase Gson, hablaremos de la deserialización de Gson (analizando objetos JSON), seguido por la serialización de Gson (creando objetos JSON).
Terminaremos discutiendo brevemente características adicionales de Gson, como anotaciones y adaptadores de tipo.

Última actualización: 23.09.2025

01.03. Gson. Diagrama de clases (e interfaces)

Jerarquía de clases Gson Jerarquía de clases Gson

Última actualización: 23.09.2025

01.04. Gson. Creación de instancias Gson


1. Introducción a la Clase Gson

La clase Gson gestiona la conversión entre JSON y objetos Java.

Se puede crear instancias de esta clase utilizando el constructor Gson(), o ppor medio de la clase com.google.gson.GsonBuilder.
El siguiente fragmento de código demuestra ambos enfoques:

Gson gson1 = new Gson();
Gson gson2 = new GsonBuilder()
    .registerTypeAdapter(Id.class, new IdTypeAdapter())
    .serializeNulls()
    .setDateFormat(DateFormat.LONG)
    .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
    .setPrettyPrinting()
    .setVersion(1.0)
    .create();

Como norma general, usa Gson() cuando se desee trabajar con la configuración predeterminada (en la mayoría de los casos), y utiliza GsonBuilder cuando se quiera anular la configuración predeterminada.

Las llamadas a los métodos de configuración se encadenan, y el método create() de GsonBuilder se llama al final para devolver el objeto Gson resultante.

2. Creación de una instancia de Gson

Antes de poder usar GSON, primero debe crearse un nuevo objeto Gson. Hay dos formas de crear una instancia de Gson:

  1. Usando el new Gson()
  2. Crear una instancia de GsonBuilder e invocar al método create() en ella.

2.1. Creación con new Gson()

Puede crearse un objeto Gson simplemente creándolo con la orden: new Gson();. Así es como se ve la creación de un objeto Gson:

Gson gson = new Gson();

Una vez que haya creado una instancia de Gson, puede comenzar a usarla para analizar y generar JSON.

2.2. Creación con GsonBuilder.create()

Otra forma de crear una instancia de Gson es crear un objeto de tipo builder GsonBuilder() y llamar a su método create(). Por ejemplo:

GsonBuilder constructorJSON = new GsonBuilder();
Gson gson = constructorJSON.create();
Opciones de configuración a GsonBuilder

El uso de un GsonBuilder es más flexible, ya que permite añadir opciones de configuración en GsonBuilder antes de crear el objeto Gson.

2.3. Configuración predeterminada (que puede cambiarse en GsonBuidler)

Gson admite la siguiente configuración predeterminada (la lista no está completa; consulta la documentación de Gson y GsonBuilder para obtener más información):

  • Gson proporciona serialización y deserialización predeterminadas para clases comunes del API, como instancias de java.lang.Enum, java.util.Map, java.net.URL, java.net.URI, java.util.Locale, java.util.Date, java.math.BigDecimal y java.math.BigInteger. Se puede cambiar la representación predeterminada registrando un adaptador de tipo (Lo veremos más adelante) a través de GsonBuilder.registerTypeAdapter(Type, Object).

  • El texto JSON generado omite todos los campos nulos. Sin embargo, conserva los nulos en los arrays porque un array es una lista ordenada. Además, si un campo no es nulo pero su texto JSON generado está vacío, se conserva el campo. Se configura Gson para serializar valores nulos llamando a GsonBuilder.serializeNulls().

  • El formato de fecha predeterminado es el mismo que java.text.DateFormat.DEFAULT. Este formato ignora la parte de milisegundos de la fecha durante la serialización. Se puede cambiar el formato predeterminado invocando GsonBuilder.setDateFormat(int) o GsonBuilder.setDateFormat(String).

  • La política predeterminada de nombrado de atributos para el formato JSON de salida es la misma que en Java. Por ejemplo, un campo de clase Java llamado versionNumber se mostrará como “versionNumber” en JSON. Las mismas reglas se aplican al mapear JSON entrante a clases Java. Se puede cambiar esta política llamando a GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy).

  • El texto JSON generado por los métodos toJson() se representa de manera compacta: se eliminan todos los espacios en blanco innecesarios. Se puede cambiar este comportamiento llamando a GsonBuilder.setPrettyPrinting().

  • Por defecto, Gson ignora las anotaciones @Since (lo veremos más adelante, para serializar sólo campos después desde determinadas versions). Puedes habilitar a Gson para que utilice estas anotaciones llamando a GsonBuilder.setVersion(double).

  • Por defecto, Gson ignora las anotaciones @Expose (serialice o no el atributo). Puedes habilitar a Gson para que serialice/deserialize solo aquellos campos marcados con esta anotación llamando a GsonBuilder.excludeFieldsWithoutExposeAnnotation().

  • Por defecto, Gson excluye campos transitorios (transient) o estáticos de la consideración para la serialización y deserialización. Puedes cambiar este comportamiento llamando a GsonBuilder.excludeFieldsWithModifiers(int...).

3. Conversión entre primitivas JSON y sus equivalentes Java: fromJson() y toJson()

Una vez que tienes un objeto Gson, se puede invocar a los métodos fromJson() y toJson() para convertir entre JSON y objetos Java, respectivamente. Por ejemplo, código siguiente presenta una aplicación sencilla que obtiene un par de objetos Gson y demuestra la conversión entre JSON y objetos Java en términos de primitivas JSON.

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import static java.lang.System.*;

public class GsonDemo {
    public static void main(String[] args) {
        Gson gson = new Gson();
        
        // Deserialization de una cadena
        String nome = gson.fromJson("\"Sylvia Plath\"", String.class);
        out.println(nome);
        
        // Serializacion de un entero
        gson.toJson(256, out); // por pantalla
        out.println(); // salto de línea.
        // Serialización
        gson.toJson("<html>", out); // por pantalla.
        out.println(); // salto de línea
        
        // Gson personalizado deshabilitando el escapado de HTML
        gson = new GsonBuilder().disableHtmlEscaping().create();
        gson.toJson("<html>", out); // Sin escapar HTML
        out.println();
    }
}
Ejercicio. Conversión de primitivas JSON

Crea un proyecto y compila el código anterior. Comprueba el resultado.

Explicación:

El listado anterior declara una clase GsonDemo cuyo método main() primero instancia Gson, manteniendo su configuración predeterminada. Luego, invoca el método genérico <T> T fromJson(String json, Class<T> classOfT) de Gson para deserializar el texto JSON especificado (en json), basado en java.lang.String, en un objeto de la clase especificada (classOfT), que en este caso es String.

La cadena JSON “Sylvia Plath” (las comillas dobles son obligatorias), que se expresa como un objeto String de Java, se convierte (sin las comillas dobles) en un objeto String de Java. Una referencia a este objeto se asigna a nome.

Después de imprimir el nombre devuelto, main() llama al método void toJson(Object src, Appendable writer) de Gson para convertir el entero (en clase envolvente) 256 (almacenado por el compilador en un objeto java.lang.Integer) en un entero JSON y mostrar el resultado en la salida estándar.

main() vuelve a invocar toJson() para mostrar una cadena de Java que contiene <html>. Por defecto, Gson escapa los caracteres HTML < y >, por lo que estos caracteres no se imprimen. Para evitar este escape, es necesario obtener un objeto Gson a través de GsonBuilder, invocando el método disableHtmlEscaping() de GsonBuilder, que hace main() a continuación. Un segundo intento de imprimir <html> revela que no hay escape.

Última actualización: 23.09.2025

01.05 Gson. Creación y lectura de objetos JSON


1. Generando JSON desde Objetos Java: toJson()

GSON puede generar JSON a partir de objetos Java empleando un objeto Gson (y viceversa).

Para generar JSON, invocamos al método toJson() del objeto Gson.

Ejemplo:

Poeta poeta = new Poeta();
poeta.setNome("Sylvia Plath");
poeta.setIdade(30);

Gson gson = new Gson();

String json = gson.toJson(poeta);

1.1. Impresión con formato “elegante”: .setPrettyPrinting()

Por defecto, la instancia Gson creada con new Gson() imprime (genera) JSON de la forma más compacta posible (¡El carácter espacio o un salto de línea, por ejemplo, ocupan espacio!. En transferencia de datos hay que economizar, sobre todo cuando se transfieren muchos archivos).

La salida compacta JSON predeterminada de Gson:

{"nome":"Sylvia Plath","idade":30}

Sin embargo, este JSON compacto puede ser difícil de leer. Por lo que GSON ofrece una opción de “impresión bonita” donde el JSON se imprime de manera que sea más legible en un editor de texto: por medio del método setPrettyPrinting() de GsonBuilder

Para crear una instancia de Gson con la opción de impresión bonita habilitada se crea por medio de la clase GsonBuilder:

Gson gson = new GsonBuilder().setPrettyPrinting().create();

Un ejemplo de cómo se vería el mismo JSON con impresión bonita:

{
  "nome": "Sylvia Plath",
  "idade": 30
}

2. De JSON a Java: fromJson()

GSON puede convertir JSON en objetos Java utilizando el método fromJson() del objeto Gson. Ejemplo de GSON parseando JSON en un objeto Java:

String textoJson = "{\"nome\":\"Sylvia Plath\", \"idade\": 30}"; // Cadena JSON a analizar

Gson gson = new Gson();

Poeta poeta = gson.fromJson(textoJson, Poeta.class); // Debemos indicar el tipo de objeto a crear

Pasos del ejemplo:

El primer parámetro de fromJson() es la fuente JSON (String, Reader, JsonReader o JsonElement).
En el ejemplo anterior, la fuente JSON es una cadena, pero existen varias versiones de este método (sobrecargado).
El segundo parámetro del método fromJson() es la clase de Java para analizar el JSON en una instancia.

La instancia Gson crea un objeto de esta clase y analiza el JSON en él. Por lo tanto, debes asegurarte de que esta clase tenga un constructor sin argumentos, o GSON no podrá usarla.

La clase Poeta sería algo así:

public class Poeta {
    private String nome = null;
    private int idade = 0;
}
Sobrecarga de métodos fromJson

Nota: el método fromJson está sobrecargado para varios tipos lectura del formato JSON: String, Reader, JsonReader y JsonElement, estas dos últimas clases del API de Gson.

Las versiones son:

public <T> T fromJson (String json, Class<T> classOfT) 
    throws JsonSyntaxException;

public <T> T fromJson (String json, Type typeOfT)
    throws JsonSyntaxException;

public <T> T fromJson (String json, TypeToken<T> typeOfT)
    throws JsonSyntaxException;

public <T> T fromJson (Reader json, Class<T> classOfT)
    throws JsonSyntaxException, JsonIOException

public <T> T fromJson (Reader json, Type typeOfT)
    throws JsonIOException, JsonSyntaxException;

public <T> T fromJson (Reader json, TypeToken<T> typeOfT)
    throws JsonIOException, JsonSyntaxException;

public <T> T fromJson (JsonReader reader, Type typeOfT)
    throws JsonIOException, JsonSyntaxException;

public <T> T fromJson (JsonReader reader, TypeToken<T> typeOfT)
    throws JsonIOException, JsonSyntaxException;

public <T> T fromJson (JsonElement json, Class<T> classOfT)
    throws JsonSyntaxException;

public <T> T fromJson (JsonElement json, Type typeOfT)
    throws JsonSyntaxException;

public <T> T fromJson (JsonElement json, TypeToken<T> typeOfT)
    throws JsonSyntaxException;

Referencias Class Gson public <T> T fromJson (String json, Class<T> classOfT) throws JsonSyntaxException

Ejercicios

Ejercicio: Gson. Transformación de Examen a JSON

Crea un proyecto Maven, igual que el anterior con JSON-B pero con GSON, con la sencilla clase Examen que contiene los siguientes atributos:

  • materia: de tipo String.
  • fecha: de tipo Date, no LocalDateTime. (Veremos por qué, pero puedes hacer una prueba con LocalDateTime).
  • participantes: de tipo List de String con los nombres de los estudiantes.

Crea los métodos get/set que consideres adecuados, así como un método toString() que devuelva la materia, la fecha seguida de la lista de participantes (emplea StringBuilder).

Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2024 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.

_NOTA: para pasar de LocalDate a Date puedes emplear la sentencia:

Date.from(LocalDateTime.of(2023, 11, 12, 9, 45).atZone(ZoneId.systemDefault()).toInstant())._

También puede hacerse así, con una instancia de Calendar y un Date o GregorianCalendar:_

Calendar calendar = Calendar.getInstance();
calendar.set(2023, Calendar.NOVEMBER, 12, 9, 45);
Date fechaConcreta = calendar.getTime();

con GregorianCalendar:

GregorianCalendar calendar = new GregorianCalendar(2023, Calendar.NOVEMBER, 12, 9, 45);
Date fechaConcreta = calendar.getTime();

Guarda el examen en una archivo JSON llamado accesoADatos.json (de manera “vistosa” y con formato de fecha yyyy-MM-dd HH:mm) mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.

Ayuda:

Ejercicio: Gson. Creación de ClasificacionDAO

Crea una clase ClasificacionDAO para guardar la clasificación de equipos de baloncesto con dos atributos privados y estáticos con los nombres de los archivos para leer y guardar la clasificación:

  • OBJECT_FILE: con el nombre de fichero clasificacion.dat para guardar el objeto Java como un flujo a objeto.
  • JSON_FILE: con el nombre de fichero clasificacion.json para guardar el objeto Java en formato JSON.

Además, debe tener un atributo privado, gson, de tipo Gson para trabajar con JSON.

El constructor por defecto debe crear ese objeto de tipo Gson, pero de modo que tenga una escritura legible.

La clase debe tener 6 métodos:

  • saveToObject(Clasificacion c): que guarda la clasificación en el fichero OBJECT_FILE. Emplea Java NIO.2 para crear el flujo de tipo Buffered.
  • saveToJSON(Clasificacion c, String file): que guarda la clasificación en el fichero recogido como argumento. Emplea el objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. La escritura debe tener un formato legible (no en una línea de texto).
  • saveToJSON(Clasificacion c): que guarda la clasificación en el fichero JSON_FILE. Emplea un objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. Puedes llamar al método anterior.
  • getFromObject(): que obtiene la clasificación a partir del fichero OBJECT_FILE. Emplea Java NIO.2 para crear el flujo de tipo Buffered.
  • getFromJSON(String file): que obtiene la clasificación a partir del fichero recogido como argumento. Emplea Java NIO.2
  • getFromJSON(): que obtiene la clasificación a partir del fichero JSON_FILE. Invoca al método anterior.

3. Exclusión de atributos en la serialización

Con GSON puede indicarse que excluya atributos de tus clases Java durante la serialización.

Existen varias formas de decirle a GSON que excluya un campo. Veremos algunas:

3.1. Atributos transient

Como hemos visto en la parte de flujos, cuando marcamos un atributo como transient no se enviará al flujo.

GSON ignora los atributos marcados como transient tanto en la serialización como en la deserialización. Así es como se ve la clase Poeta que usamos en el primer ejemplo, con el campo “nome” marcado como transient:

public class Poeta {
    public transient String nome = null; // no se serializa
    public int idade;
}

3.2. Anotación @Expose: GsonBuilder.excludeFieldsWithoutExposeAnnotation()

  • La anotación @Expose de GSON (com.google.gson.annotations.Expose) se puede usar para marcar un atributo para que se exponga o no (se incluya o no) al serializar o deserializar un objeto.

  • La anotación no tiene efecto a menos que se construya un objeto Gson con GsonBuilder y se invoque al método GsonBuilder.excludeFieldsWithoutExposeAnnotation():

  • La anotación @Expose puede tener dos parámetros: serialize y deserialize, ambos son booleanos que pueden tener los valores true o false:

    • El parámetro serialize de la anotación @Expose indica si el atributo anotado con la @Expose debe incluirse cuando el objeto se serializa.
    • El parámetro deserialize anota si ese atributo debe leerse cuando el objeto se deserializa.

Por ejemplo, la anotación @Expose:

  • @Expose(serialize = true);
  • @Expose(serialize = false);
  • @Expose(deserialize = true);
  • @Expose(deserialize = false);
  • @Expose(serialize = true, deserialize = false);
  • @Expose(serialize = false, deserialize = true);

Ejemplos de clase que utiliza la anotación @Expose:

public class Estudiante {
   @Expose private String nome; // Se incluirá en la serialización y deserialización
   @Expose(serialize = false) private String apelidos;
   @Expose (serialize = false, deserialize = false) private String email; // no
   private String password; // ... ? NO LO SERIALIZA NI DESERIALIZA
 }
@Expose en objetos creados con new Gson()

Si se crea un objeto Gson con new Gson(), los métodos toJson() y fromJson() utilizarán los atributos del objeto para la serialización y deserialización (en el ejemplo anterior, nome, apelidos, email y password). Sin embargo, si se generó el objeto Gson con Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() los métodos toJson() y fromJson() de Gson excluirán el atributo password. Esto se debe a que el atributo password, que no está marcado con la anotación @Expose.
Gson también excluirá apelidos e email de la serialización ya que serialize está configurado en false. De manera similar, Gson excluirá email la deserialización ya que deserialize está configurado en false.

public class Poeta {

    @Expose(serialize = false, deserialize = false)
    public String nome = null;

    @Expose(serialize = true, deserialize = true)
    public int idade = 31;
}

Observa la anotación @Expose sobre los atributos, indicando si el campo dado debe incluirse al serializar o deserializar.

Para que GSON tenga en cuenta a las anotaciones @Expose, se debe crear una instancia de Gson utilizando la clase GsonBuilder. Así es cómo se ve eso:

GsonBuilder builder = new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
Gson gson = builder.create();

Ten en cuenta que esta configuración hace que GSON ignore todos los atributos que no tengan una anotación @Expose. Para que un campo se incluya en la serialización o deserialización, debe tener una anotación @Expose sobre él.

3.3 Exclusión de campos con GsonBuilder.setExclusionStrategies()

Otra forma de excluir un campo de una clase de la serialización o deserialización en GSON es usar GsonBuilder para construir el objeto Gson y configurar una ExclusionStrategy en un GsonBuilder.

ExclusionStrategy es una interfaz, por lo que hay que crear una clase que implemente la interfaz ExclusionStrategy.

Por ejemplo, implementando la interfaz ExclusionStrategy con una clase anónima:

ExclusionStrategy politicaExclusion = new ExclusionStrategy() {
    public boolean shouldSkipField(FieldAttributes fieldAttributes) {
        if("password".equals(fieldAttributes.getName())){
            return true;
        }
        return false;
    }

    public boolean shouldSkipClass(Class aClass) {
        return false;
    }
};

Dentro del método shouldSkipField() de la implementación de ExclusionStrategy, en el ejemplo, verifica si el nombre de campo dado es “password”. Si es así, ese campo se excluye de la serialización y deserialización.

Para usar la implementación de ExclusionStrategy, se crea un GsonBuilder y establece la ExclusionStrategy en él usando el método setExclusionStrategies(), de la siguiente manera:

GsonBuilder builder = new GsonBuilder();
builder.setExclusionStrategies(politicaExclusion);
Gson gson = builder.create();

La variable politicaExclusion debe apuntar a una implementación de la interfaz ExclusionStrategy.

El objeto de tipo FieldAttributes tiene métodos para obtener el nombre del campo, la clase que lo declara, el tipo declarado, si tiene modificador o las anotaciones que tiene el campo. Eso nos permite hacer filtros más dinámicos combinando esos métodos.

Clase FieldAttributes

La interfaz ExclusionStrategy tiene dos versiones sobrecargadas del método shouldSkipField, una con el parámetro de tipo Class y tomando un objeto de tipo FieldAttributes como parámetro.

Un objeto de tipo FieldAttributes tiene método para obtener el nombre ()getName()), su valor como cadena (toString()), la clase que lo declara (getDeclaredClass()), el tipo declarado (getDeclaredType()), si tiene modificador (hasModifier (int modifier)) o las anotaciones que tiene el campo (getAnnotations()). Eso nos permite hacer filtros más dinámicos combinando esos métodos.

3.4. Serialización de Campos Nulos

Por defecto, el objeto Gson no serializa campos con valores nulos a JSON. Si un campo en un objeto Java es nulo, Gson lo excluye.

Se puede obligar a serializar a Gson valores nulos a través de GsonBuilder. Por ejemplo:

GsonBuilder builder = new GsonBuilder();

builder.serializeNulls(); // esto es lo que vemos ahora.

Gson gson = builder.create();

Materia ad = new Materia();
ad.nome = null;

String json = gson.toJson(ad);
System.out.println(json);

Una vez que se ha llamado a serializeNulls(), la instancia de Gson creada por GsonBuilder incluirá campos nulos en el JSON serializado.

La salida del ejemplo anterior sería:

{"nome":null,"horas":9, "profesor": "javhoz"}

Observa cómo el campo nome es nulo.

Gestión de equipos y clasificaciones con archivos JSON

Se trata de completar la tarea de Clasificación de equipos con archivos JSON, creando clases DAO que trabajen con archivos JSON.

Haga un programa para la gestión y clasificación de las ligas, como la ACB. Las clasificaciones de los equipos se guardan en archivos binarios o de texto, según decidas. Por ejemplo: Liga ACB.json.

a) Declare una clase Equipo con los atributos mínimos necesarios: nombre, victorias, derrotas, puntosAfavor a favor, puntosEnContra puntos en contra. Puedes añadir los atributos que te interesen, como ciudad, etc. Tienes libertad para hacerlo, pues, además, te puede servir como práctica. En una liga de fútbol, por ejemplo, se podría añadir el campo estadio y los puntos a favor serían los goles a favor.

Además, ten en cuenta que los atributos puntos, partidos jugados y diferencia de puntos son atributos derivados que se calculan a partir de los partidos ganados, perdidos, puntos a favor y puntos en contra.

Cree los métodos que considere oportunos, pero tome decisiones sobre los métodos get/set necesarios. Así, haz un método que devuelva los puntos, getPuntos, un método getPartidosJugados que devuelva el número de partidos jugados y un método getDiferenciaDePuntos, que devuelva la diferencia de puntos. Obviamente, por ser atributos/propiedades derivados/as, no tienen sentido los métodos de tipo “set” para ellos.

Debe tener, al menos, un constructor para la clase equipo que recoja el nombre y otro que recoja todas las propiedades. Debe existir un constructor por defecto.

Para poder ordenar los equipos debe implantar la interface Comparable<Equipo>. Piense que debe ordenar por puntos y, a igualdad de puntos, por diferencia de puntos encestados. Además, debe implantar la interfaz Serializable. Lo mismo con la clase siguiente, Clasificacion, que debe implementar la interfaz Serializable.

Sobrescribe el método equals para que se considere que dos Equipos son iguales si tienen el mismo nombre (sin distinguir mayúsculas de minúsculas). Haz lo mismo con hashCode.

b) Declare una clase Clasificacion, con los atributos:

  • equipos de tipo Set de Equipo (será de tipo TreeSet), aunque debe existir un constructor que permita crear una clasificación con los equipos que se desee.

  • competicion de tipo String que recoja el nombre de la competición. Por defecto, la competición debe ser “Liga ACB”.

  • Defina los métodos para añadir equipos a la clasificación, addEquipo, así como los métodos para eliminar equipo, removeEquipo, y sobrescriba el método toString que devuelva la cadena de la clasificación (StringBuilder)

Los constructores de Clasificación deben crear el conjunto de equipos como tipo TreeSet, para que los ordene automáticamente.

c) Interface DAO<T, K> (Data Access Object) es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a los datos. Con los siguientes métodos:

import java.util.List;

/**
 * Dao genérico.
 * Esta clase define los métodos que deben implementar las clases que quieran
 * ser un Dao.
 * La T es el tipo de objeto que se va a manejar y la K es el tipo de clave
 * primaria.
 * @param <T>
 * @param <K>
 */
public interface Dao<T, K> {

  T get(K id);
  List<T> getAll();
  boolean save(T obxecto);
  boolean delete(T obx);
  boolean deleteAll();
  boolean deleteById(K id);
  void update(T obx);


}

e) Crea una clase EquipoJSONDAO que implemente la interfaz DAO<Equipo, String>. Debe implantar los métodos de la interfaz. Esta clase debe tener un atributo final, path, de tipo Path con la ruta completa al archivo de datos JSON en el que se guarda la clasificación completa.

f) Cree una clase ClasificacionJSONeDAO que implemente la interfaz DAO<Clasificacion, String>. Debe tener un atributo final con la ruta en la que se guardan los datos de la clasificación: ruta. El nombre del archivo debe ser el nombre de la competición seguido de .json. Constructor al que se le pasa la ruta, etc. Para facilitar el trabajo. los métodos de la clase ClasificacionFileDAO pueden hacer uso de la clase EquipoFileDAO.

g) Haz las pruebas necesarias para comprobar el correcto funcionamiento.

Como mejora, intenta hacerlo con una aplicación gráfica.

A ser posible emplea el patrón Factory para crear los objetos DAO:

// Ejemplo de Factory general
public class DaoFactory {

  public static Dao getDao(String tipo){
    if(tipo.equalsIgnoreCase("equipo")){
      return new EquipoJSONDAO();
    } else if(tipo.equalsIgnoreCase("clasificacion")){
      return new ClasificacionJSONDAO();
    }
    return null;
  }
}

// Ejemplo de Factory para Clasificación
public class ClasificacionDAOFactoy {

  public static Dao<Clasificacion, String> getClasificacionDAO(String tipo) {
    if (tipo.equalsIgnoreCase("file")) {
      return ClasificacionFileDAO.getInstance();
    } else if (tipo.equalsIgnoreCase("json")) {
      return ClasificacionFileDAO.getInstance();
    } else{
      return null;
    }
  }

}
Ejercicio: serialización JSON del Sudoku

A partir del ejercicio de la tarea del Sudoku, y por medio de las dos estrategias vistas anteriormente, haz que no serialice en un archivo JSON el alfabero del Sudoku y sólo lo haga con los datos. Además, debe escribirlo de manera “legible”.

Crea una clase SudokuDAO con las siguientes características:

  • JSON_FILE: con el nombre de fichero sudoku.json para guardar el objeto Java en formato JSON.

Además, debe tener un atributo privado, gson, de tipo Gson para la trabajar con JSON.

El constructor por defecto debe crear ese objeto de tipo Gson, pero de modo que tenga una escritura legible.

La clase debe tener los siguientes métodos:

Para trabajar con objetos Java:

  • saveToObject(Sudoku c, String ruta): que guarda el sudoku en el fichero recogido como argumento. Emplea Java NIO.2 para crear el flujo de tipo Buffered. ¿Cuál es la diferencia entre Files.newOutputStream() y new FileOutputStream()?
  • getFromObject(String ruta): recoge la ruta al fichero y de devuelve el objeto guardado en dicho fichero mediante el método anterior. Emplea Java NIO.2 para crear el flujo de tipo Buffered.

Para trabajar con objetos JSON:

  • saveToJSON(Sudoku c, String file): que guarda el sudoku en el fichero recogido como argumento. Emplea el objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea de código. La escritura debe tener un formato legible (no en una línea de texto).
  • saveToJSON(Sudoku c): que guarda el sudoku en el fichero JSON_FILE. Emplea un objeto de tipo Gson y Java NIO.2 para guardar la cadena, a ser posible en una línea. Puedes llamar al método anterior.
  • getFromJSON(String file): que obtiene el sudoku a partir del fichero recogido como argumento. Emplea Java NIO.2
  • getFromJSON(): que obtiene el sudoku a partir del fichero JSON_FILE. Invoca al método anterior.

Pata trabajar con archivos de texto:

  • Sudoku getFromTXT(String ruta): lee el sudoku de un archivo de texto en el que cada línea son los caracteres de cada fila y devuelve el sudoku equivalente.
Reto: crea un método que resuelva el sudoku.

A modo de ejemplo, puedes ver este código que imprime la soluciones por pantalla.

    public void resolver() throws Exception {
        // Los hijos de cada Sudoku
        List<Sudoku> hijos = getChildren();

        for (Sudoku hijo : hijos) {
            if (hijo.isValid() && hijo.isCompleted()) {
                System.out.println("Solución:");
                System.out.println(hijo);
            } else if (hijo.isValid()) {
                hijo.resolver();
            }
        }

    }
  • Crea un atributo para guardar las soluciones, List<Sudoku> solucions;, y crea el atributo en el constructor (mejor).
  • Crea un método get para las soluciones.
  • Haz que el método resolver() guarde las soluciones hijo en la lista de soluciones.
  • Implanta un método en SudokuDAO que implante un método para guardar las soluciones en un archivo JSON: saveSolutionsToJSON(String ruta).
Última actualización: 23.09.2025

01.06. Gson. Transformación de objetos JSON personalizada


0. Introducción. GsonBuilder#registerTypeAdapter (Type, Object)

GsonBuilder dispone de un método:

public GsonBuilder registerTypeAdapter (Type tipo, Object tipoDeAdaptador)

Que se emplea para la serialización o deserialización personalizada.
Este método puede registrar varios tipos de adaptadores:

  1. Adaptadores de tipo: TypeAdapter, clase abstracta empleada para personalizar la adaptación de tipos integrados, implantando los métodos write (JsonWriter out, T valor) y read(JsonReader reader):

    • El método write se emplea para serializar un objeto de tipo T en un JSON: public abstract void write (JsonWriter out, T value) throws IOException. Por ejemplo, para serializar un objeto de tipo Persona:
    public void write(JsonWriter out, Persona persona) throws IOException {
            if(out == null || persona == null) {
                writer.nullValue();
                return;
            }
            out.beginObject();
            out.name("nombre").value(persona.getNombre());
            out.name("edad").value(persona.getEdad());
            out.endObject();
    }

    Los métodos principales de JsonWriter son: beginArray(), endArray(), beginObject(), endObject(), name(String name), value(String value), value(Boolean value), value(double value), value(long value), nullValue(), setLenient(boolean lenient), setIndent(String indent), setSerializeNulls(boolean serializeNulls), close(), flush(). El método name(String name) se emplea para escribir el nombre de un atributo en un objeto JSON y el método value(T value) para escribir el valor de un atributo en un objeto JSON.

  • El método read se emplea para deserializar un JSON en un objeto de tipo T: public abstract T read(JsonReader in) throws IOException. Por ejemplo, para deserializar un objeto de tipo Persona:

    public Persona read(JsonReader in) throws IOException {
        if(in == null) {
            return null;
        }
        Persona persona = new Persona();
        in.beginObject();
        while (in.hasNext()) {
            String name = in.nextName();
            switch (name) {
                case "nombre":
                    persona.setNombre(in.nextString());
                    break;
                case "edad":
                    persona.setEdad(in.nextInt());
                    break;
                default:
                    in.skipValue();
                    break;
            }
        }
        in.endObject();
        return persona;
    }

    Los métodos principales de JsonReader son: beginArray(), endArray(), beginObject(), endObject(), hasNext(), nextName(), nextString(), nextBoolean(), nextDouble(), nextInt(), skipValue(), setLenient(boolean lenient), close(), peek(), skipValue(). El método nextName() se emplea para leer el nombre de un atributo en un objeto JSON y los métodos nextString(), nextBoolean(), nextDouble(), nextInt() para leer el valor de un atributo en un objeto JSON. Se puede usar el método peek() para ver el siguiente token sin consumirlo y tomar decisiones::

    JsonToken token = in.peek();
    
    if (token == JsonToken.NULL) {
        in.nextNull();
        return null;
    }
  1. Creadores de instancia: InstanceCreator<T>, interfaz que debe implantarse para crear instancias de una clase sin constructor por defecto. Siempre, si fuese posible, es mejor implantar un constructor por defecto.
  2. Serialización y deserialización personalizada: JsonSerializer<T> y un JsonDeserializer<T>. Interfaces que representa un serializador y deserializador personalizado para JSON. Debe escribir un serializador/deserializador personalizado si no estás satisfecho con la serialización predeterminada realizada por Gson.

Se utiliza mejor cuando un único objeto TypeAdapter implementa todas las interfaces necesarias para la serialización personalizada con Gson.
Si se registró previamente un adaptador de tipo para el tipo especificado, este será sobrescrito.

Este método registra solo el tipo especificado y ningún otro: ¡debes registrar manualmente los tipos relacionados! Por ejemplo, las aplicaciones que registran boolean.class también deben registrar Boolean.class.

JsonSerializer y JsonDeserializer son “a prueba de nulos”. Esto significa que al intentar serializar null, Gson escribirá un JSON null y no se llamará al serializador. De manera similar, al deserializar un JSON null, Gson emitirá null sin llamar al deserializador. Si se desea manejar valores nulos, en su lugar, se debe usar un TypeAdapter.

public GsonBuilder registerTypeAdapter (Type type, Object typeAdapter) {
    // Implementación del método
    // ...
    return this; // Devuelve una referencia a este objeto GsonBuilder
}

Empezaremos viendo cómo restringir la serialización y deserialización por versión de la clase y pasaremos a ver ejemplos de adaptadores personalizados.

1. Soporte de versiones en GSON: @Since y @Until

GSON permite un control sencillo de versiones de las clases para los objetos Java que lee y escribe. El soporte de versiones en GSON significa que se pueden marcar los atributos de las clases Java con un número de versión y luego hacer que GSON incluya o excluya campos de tus clases Java según su número de versión.
Estas anotaciones son útiles para gestionar el control de versiones de las clases JSON.

Se precisan hacer dos cosas:

  1. Añadir la anotación @Since o la anotacion @Until al atributo: @Since(x.x) o @Until(x.x)
    1. @Until indica el número de versión HASTA que un miembro o un tipo debe estar presente. Si Gson se crea con un número de versión igual o superior al valor almacenado de la anotación @Until, el campo se ignorará en la salida JSON.
    2. @Since indica el número de versión DESDE que un miembro o un tipo debe estar presente.
  2. Indicarle al GsonBuilder las versión a admitir: public GsonBuilder setVersion (double version)

(1) Ejemplo de la clase Persoa con sus campos anotados con las anotaciones @Since y @Until:

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;

public class Persoa {
    // He puesto los atributos como públicos para simplificar
    // el código, pero no se recomienda en absoluto.
    @Since(1.0)
    public String nome = null;

    @Since(1.0)
    public String apelidos = null;

    @Until(2.0)
    public String cidade = null;

    @Since(3.0)
    public String email = null;
}

(2) En segundo lugar, debes crear un GsonBuilder y decirle a qué versión (desde o hasta) debería serializar y deserializar.

Por ejemplo:

GsonBuilder builder = new GsonBuilder();
// Versión 2.0, entonces serializa/deserializa 
// los que tienen un @Since menor o igual a 2.0 o @Until 
// mayor que 2.0 
builder.setVersion(2.0);

Gson gson = builder.create();

La instancia de Gson creada a partir del GsonBuilder anterior solo incluirá campos que estén anotados con @Since(2.0) o con un número de versión inferior a 2.0, así como los campos que tengan @Until superior a 2.0 (no inclúido).

En el ejemplo de la clase Persoa anterior los campos nome y apelidos serán incluidos, no así cidade porque tiene un valor igual (no superior) a 2.0. El campo email está anotado con la versión 3.0, que es posterior a 2.0, por lo que GSON también excluirá el campo email.

Ejemplo de cómo serializar un objeto Persoa a JSON y ver el JSON generado:

Persoa persoa = new Persoa();
persoa.nome = "Anne";
persoa.apelidos = "Sexton";
persoa.cidade = "Santiago";
persoa.email = "anne@doe.com";

GsonBuilder builder = new GsonBuilder();
builder.setVersion(2.0);

Gson gson = builder.create();

String persoaJson = gson.toJson(persoa);

System.out.println(persoaJson);

Este ejemplo imprimirá la siguiente cadena JSON:

{"nome":"Anne","apelidos":"Sexton"}

Observa cómo GSON excluyó el campo email y cidade en el JSON generado.

Excluir campos basados en la versión funciona de la misma manera para leer JSON en objetos Java (deserialización). Observa la siguiente cadena JSON que contiene todos los campos, incluido el campo email:

{"nome":"Anne","apelidos":"Sexton","cidade":"Santiago","email":"anne@doe.com"}

Si se intenta leer un objeto Persoa con la instancia de Gson anterior, el campo email y el campo cidade no se leerán incluso si está presente en la cadena JSON.
Así se vería la lectura de un objeto Persoa con la instancia de Gson anterior:

String persoaJson2 = "{\"nome\":\"Anne\",\"apelidos\":\"Sexton\",\"cidade\":\"Santiago\",\"email\":\"anne@doe.com\"}";

Person persoaLeida = gson.fromJson(persoaJson2, Persoa.class);

Comprueba el resultado.

2. Creación de objetos personalizados en GSON: InstanceCreator

GSON de manera prederminada crea los objetos a partir de un JSON invocando al constructor por defecto.
En muchos casos la clase no tiene un constructor predeterminado, o se desea realizar alguna configuración predeterminada del objeto, o se desea crear una instancia de una subclase en su lugar.
Para eso Gson tiene una interface: com.google.gson.InstanceCreator.

Un objeto de tipo InstanceCreator en GSON es un objeto de tipo Factory. Un creador de instancias tiene que implementar la interfaz InstanceCreator (com.google.gson.InstanceCreator).

Por ejemplo:

import com.google.gson.InstanceCreator;

public class CreadorDePoetas implements InstanceCreator<Poeta> {
    public Poeta createInstance(Type tipo) {
        Poeta poeta = new Poeta();
        poeta.setCategoria("Poesía");
        return poeta;
    }
}

Se puede usar la clase CreadorDePoetas registrándola en un GsonBuilder con el método registerTypeAdapter antes de crear la instantcia de tipo Gson: gsonBuilder.registerTypeAdapter(Poeta.class, new CreadorDePoetas());

GsonBuilder gsonBuilder = new GsonBuilder();

gsonBuilder.registerTypeAdapter(Poeta.class, new CreadorDePoetas());

Gson gson  = gsonBuilder.create();

El objeto de tipo Gson del ejemplo anterior utilizará la instancia CreadorDePoetas para crear objetos de tipo Poeta.
Comprúebalo con el siguiente código (haciendo uso del código anterior):

String poetaJson = "{ \"nome\" : \"Anne Sexton\",  \"idade\" : 45}";

Poeta poeta = gson.fromJson(poetaJson, Poeta.class);

// se supone que poeta tiene un campo denominado categoria.
System.out.println(poeta.getCategoria());

El valor predeterminado de la propiedad categoria es nulo y la cadena JSON no contiene una propiedad categoria. Sin embargo, se asigna el valor para la propiedad categoria establecido dentro del método createInstance() de CreadorDePoetas (Poesía).

3. Serialización y Deserialización personalizados: JsonSerializer y JsonDeserializer

GSON ofrece la posibilidad de utilizar serializadores y deserializadores personalizados.
Los serializadores personalizados pueden convertir valores Java a JSON personalizado, y los deserializadores personalizados pueden convertir JSON personalizado de nuevo a valores Java.

3.1. Serializador personalizado

Un serializador personalizado en GSON debe implementar la interfaz funcional JsonSerializer. La interfaz JsonSerializer:

public interface JsonSerializer<T> {
    public JsonElement serialize(T valor, Type tipo,
        JsonSerializationContext jsonSerializationContext) {
    }
}

JSONElement es una clase abstracta que representa un elemento JSON. Subclases de JSONElement son:

  • JsonArray: representa un array JSON, Podemos añadir elementos a un JsonArray con el método add() y obtener un elemento con el método get(int i). También es posible obtener el array como un único elemento Java si contiene un único elemento: getAsBoolean(), getAsCharacter(), getAsDouble(), getAsFloat(), getAsInt(), getAsString(), etc.
  • JsonNull/JsonNull.INSTANCE: representa un valor nulo en JSON.
  • JsonObject: representa un objeto JSON. Podemos añadir elementos a un JsonObject con el método add(String property, JsonElement value) o addProperty(String property, T value) y obtener un elemento con el método get(String nombreMiembro) o como Array, Objeto y tipo primitivo con los métodos getAsJsonArray(), getAsJsonObject(), getAsJsonPrimitive().
  • JsonPrimitive, que son Boolean, Character, Number o String y permite crear un JSON primitivo: new JsonPrimitive(1), new JsonPrimitive("Wittgenstein"), new JsonPrimitive(true), new JsonPrimitive('a').

Por ejemplo, para declarar un serializador personalizado que pueda serializar valores booleanos:

public class BooleanSerializer implements JsonSerializer<Boolean> {

  public JsonElement serialize(Boolean aBoolean, Type tipo,
    JsonSerializationContext jsonSerializationContext) {
    if(aBoolean){
       return new JsonPrimitive(1);
    }
      return new JsonPrimitive(0);
  }
}

Observa cómo el parámetro de tipo T se sustituye con la clase Boolean en dos lugares.

Dentro del método serialize(), puedes convertir el valor (un Boolean en este caso) a un JsonElement, que el método serialize() debe devolver. En el ejemplo anterior, utilizamos un JsonPrimitive, que también es un JsonElement. Como puedes ver, los valores booleanos verdaderos se convierten en 1 y los falsos en 0, en lugar de true y false normalmente usados en JSON.

JsonElement

Existen 4 subclases de JsonElement que pueden ser devueltas: JsonArray, JsonNull.INSTANCE, JsonObject, JsonPrimitive, que son Boolean, Character, Number o String.
Ten en cuenta que el método serialize devuelve un objeto de tipo JsonElement.

El Type tipo es el tipo de objeto que se está serializando. En la mayoría de los casos, se puede ignorar este parámetro, pero permite obtener información sobre el nombre de la clase y los parámetros de tipo.

Registrar este serializador personalizado se hace de la siguiente manera (empleando un objeto del tipo BooleanSerializer):

GsonBuilder builder = new GsonBuilder();

builder.registerTypeAdapter(Boolean.class, new BooleanSerializer()) ;

Gson gson = builder.create();

Se realiza una llamada a registerTypeAdapter() para que registra el serializador personalizado con GSON.

Con clases anónimas:

GsonBuilder builder = new GsonBuilder();

builder.registerTypeAdapter(Boolean.class, new JsonSerializer<Boolean>() {
    @Override
    public JsonElement serialize(Boolean aBoolean, Type tipo,
            JsonSerializationContext jsonSerializationContext) {
        if (aBoolean) {
            return new JsonPrimitive(1);
        }
        return new JsonPrimitive(0);
    }
}
);

Gson gson = builder.create();

Con clases expresiones lambda:

GsonBuilder builder = new GsonBuilder();

builder.registerTypeAdapter(Boolean.class, 
        (JsonSerializer<Boolean>) (aBoolean, tipo, jsonSerializationContext) -> {
    if (aBoolean) {
        return new JsonPrimitive(1);
    }
    return new JsonPrimitive(0);
});

Gson gson = builder.create();

Una vez registrado, la instancia de Gson creada a partir de GsonBuilder utilizará el serializador personalizado. Para ver cómo funciona, utilizaremos la siguiente clase POJO:

public class Usuario {
    public String usuario = null;
    public Boolean esSuperUsuario = false;
}

Así es como se ve la serialización de una instancia de Usuario:

Usuario pojo = new Usuario();
pojo.usuario = "abc";
pojo.esSuperUsuario = false;

String pojoJson = gson.toJson(pojo);

System.out.println(pojoJson);

La salida impresa de este ejemplo sería:

{"usuario":"abc","esSuperUsuario":0}

Observa cómo el valor false de esSuperUsuario se convierte en un 0.

3.2. Deserializador personalizado

GSON también permite deserializadores personalizados.
Un deserializador personalizado debe implementar la interfaz JsonDeserializer.

Debe escribir un deserializador personalizado si se quiere modificar la deserialización predeterminada realizada por Gson. Además, también se debe registrar el deserializador a través de GsonBuilder.registerTypeAdapter(Type, Object).

La interfaz JsonDeserializer:

public interface JsonDeserializer<T> {
    
    public Boolean deserialize(JsonElement jsonElement, 
        Type tipo, JsonDeserializationContext jsonDeserializationContext) 
        throws JsonParseException;

}

Implementar un deserializador personalizado para el tipo Boolean se vería así:

public class BooleanDeserializer implements JsonDeserializer<Boolean> {

    public Boolean deserialize(JsonElement jsonElement, Type tipo,
    JsonDeserializationContext jsonDeserializationContext)
    throws JsonParseException {

        return jsonElement.getAsInt() == 0 ? false : true;
    }
}

Ahora, como se ha comentado, se debe registrar el deserializador a través de GsonBuilder.registerTypeAdapter(Type, Object):

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Boolean.class, new BooleanDeserializer());

Gson gson = builder.create();

Y así es como se ve analizar una cadena JSON con la instancia de Gson creada:

String jsonSource = "{\"usuario\":\"abc\",\"esSuperUsuario\":1}";

Usuario pojo = gson.fromJson(jsonSource, Usuario.class);

System.out.println(pojo.esSuperUsuario);

La salida impresa de este ejemplo de deserialización personalizada de GSON sería:

true

… ya que el 1 en la cadena JSON se convertiría en el valor booleano true.

Ejemplo avanzado

Veamos un ejemplo más avanzado en dónde la serialización y deserialización resulta más útil. La clase Id definida a continuación tiene dos campos: clase y valor.

public class Id<T> {
   private final Class<T> clase;
   private final long valor;
   
   public Id(Class<T> clase, long valor) {
     this.clase = clase;
     this.valor = valor;
   }
   
   public long getvalor() {
     return valor;
   }
}

La deserialización predeterminada de Id(com.otto.MiClase.class, 20L) requerirá que la cadena JSON sea {"clase":"com.otto.MiClase","valor":20}.
Supongamos que se conoce el tipo del campo en el que se deserializará el Id y, por lo tanto, sólo se desea deserializarlo a partir de una cadena JSON 20.

Se puede hacer escribiendo un deserializador personalizado:

public class IdDeserializer implements JsonDeserializer<Id> {
   public Id deserialize(JsonElement json, Type tipoDeT, JsonDeserializationContext context)
       throws JsonParseException {
     long idValor = json.getAsJsonPrimitive().getAsLong();
     return new Id((Class) tipoDeT, idValor);
   }
}

También se debe registrar el objeto de tipo IdDeserializer con Gson:

Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdDeserializer()).create();
TypeAdapter o JsonSerializer/JsonDeserializer

Las nuevas aplicaciones deberían emplear TypeAdapter, incorporado en la versión 2.1 de Gson, cuya API de transmisión es más eficiente que la API de las interfaces JsonDeserializer<T> y JsonSerializer<T>, pues no requiere la creación de objetos intermedios y emplea flujo de salida y entrada de JSON directamente, con menor consumo de memoria.

Ejercicio. Clase Examen con JsonSerializer y JsonDeserializer de LocalDateTime

Modifica la clase Examen que contiene los siguientes atributos:

  • materia: de tipo String.
  • fecha: LocalDateTime, ahora LocalDateTime.
  • participantes: de tipo List de String con los nombres de los estudiantes.

Para que la fecha la guarde en formato LocalDateTime, no Date.

Para que la serialización/deserialización funcione correctamente, debes crear una clase que implante las interfaces siguientes:

public class LocalDateTimeTypeAdapter 
        implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime>;

Emplea el formato siguiente para la fecha en la serialización del objeto de tipo LocalDateTime:

private static final DateTimeFormatter formato 
          = DateTimeFormatter.ofPattern("d:MM:uuuu HH:mm:ss");

Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.

Guarda el examen en una archivo JSON llamado accesoADatos.json, de manera “vistosa” y con formato de fecha anterior mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.

Ayuda:

Ejercicios con JsonSerializer y JsonDeserializer

Ejercicio 1: Serialización y deserialización básica Serializar y deserializar una clase sencilla con atributos básicos.

Crea una clase Persona con atributos nombre y edad. Implementa un JsonSerializer y un JsonDeserializer para esta clase, personalizando los nombres de los atributos en el JSON resultante, de modo que aparezcan como name y age en formato JSON.

Ejercicio 2: Serialización y deserialización de objetos anidados Manejar una clase que contiene otro objeto como atributo.

Crea una clase Direccion con atributos calle y ciudad. Añade un atributo de tipo Direccion. Implementa los serializadores y deserializadores necesarios para manejar esta estructura de modo que la dirección tenga el nombre address y aparezca como una cadena de texto con el formato calle, ciudad.

Ejercicio 3: Serialización y deserialización de listas Manejar una clase que contiene una lista de objetos.

Añade a la clase Persona una lista de objetos Persona llamados amigos. Implementa los serializadores y deserializadores para manejar la lista de amigos en el JSON. Haz que la lista de amigos la represente como un array de objetos JSON.

Ejercicio 4: Serialización y deserialización de números personalizados Personalizar la serialización y deserialización de números.

Crea una clase Producto con atributos nombre y precio. Implementa un JsonSerializer y un JsonDeserializer que formateen el precio como una cadena con dos decimales en el JSON.

Ejercicio 5: Serialización y deserialización de arrays Manejar una clase que contiene un array de objetos.

Añade a la clase persona un atributo hobbies. Implementa los serializadores y deserializadores para manejar el array de hobbies en el JSON para que aparezca como una lista de cadenas de texto separadas por guion.

4. Adaptadores de tipo: clase TypeAdapter

El API de Gson incorpora una clase, para declarar adaptaciones de tipos de datos personalizadas, la clase abstracta TypeAdapter.
Dicha clase tiene dos métodos abstractos “read” y “write.

Definiendo la forma JSON de un tipo

Por defecto, Gson convierte las clases de la aplicación a JSON utilizando sus adaptadores de tipo integrados. Si la conversión JSON predeterminada de Gson no es adecuada para un tipo, debe extenderse esta clase para personalizar la conversión.

Por ejemplo, un adaptador de tipo para un punto (X, Y):

// Adaptador de la clase Point
public class PointAdapter extends TypeAdapter<Point> {
  
  // Implantación del método read:
  public Point read(JsonReader reader) throws IOException {
    if (reader.peek() == JsonToken.NULL) { // si el token es null, lo lee y sale.
      reader.nextNull();
      return null;
    }
    String xy = reader.nextString(); // lee la cadena y la consume.
    String[] coords = xy.split(",");
    int x = Integer.parseInt(coords[0]);
    int y = Integer.parseInt(coords[1]);
    return new Point(x, y);
  }

  // Implantación del método write para escribir el Objeto Java.
  public void write(JsonWriter writer, Point punto) throws IOException {
    if (punto == null) {
      writer.nullValue(); // codifica null
      return;
    }
    String xy = punto.getX() + "," + punto.getY();
    writer.value(xy); // Codifica la cadena (devuelve el JsonWriter que podemos concatenar)
  }

}

Con este adaptador de tipo registrado, Gson convertirá los puntos a JSON como cadenas como “5,8” en lugar de objetos como {“x”:5,“y”:8}. En este caso, el adaptador de tipo vincula una clase Java a un valor JSON compacto.

El método read() debe leer exactamente un valor y write() debe escribir exactamente un valor.

  • Para tipos primitivos, esto significa que los readers deben hacer exactamente una llamada a nextBoolean(), nextDouble(), nextInt(), nextLong(), nextString() o nextNull(). Estos métodos devuelven el valor boolean, double, int, long, String o null del siguiente token, consumiéndolo.
  • Los writers deben hacer exactamente una llamada a value() o nullValue(). “value” codifica el valor y lo escribe directamente.
  • Para arrays, los adaptadores de tipo deben comenzar con una llamada a beginArray(), convertir todos los elementos y finalizar con una llamada a endArray().
  • Para objetos, deben comenzar con beginObject(), convertir el objeto y finalizar con endObject(). No convertir un valor o convertir demasiados valores puede hacer que la aplicación se bloquee.

Los adaptadores de tipo deben estar preparados para leer null desde el flujo y escribirlo en el flujo. Alternativamente, deben utilizar el método nullSafe() al registrar el adaptador de tipo con Gson. Si la instancia de Gson ha sido configurada con GsonBuilder.serializeNulls(), estos nulos se escribirán en el documento final. De lo contrario, el valor (y el nombre correspondiente al escribir en un objeto JSON) se omitirá automáticamente. En ambos casos, el adaptador de tipo debe manejar null.

Los adaptadores de tipo deben ser sin estado y seguros para subprocesos; de lo contrario, las garantías de seguridad para subprocesos de Gson podrían no aplicarse.

Para usar un adaptador de tipo personalizado con Gson, debes registrarlo con un GsonBuilder:

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Point.class, new PointAdapter());
// Si PointAdapter no comprobó los nulos en sus métodos de lectura/escritura, debes usar en su lugar
// builder.registerTypeAdapter(Point.class, new PointAdapter().nullSafe());
...
Gson gson = builder.create();
Ejercicio con TypeAdapter

Modifica la clase Examen que contiene los siguientes atributos:

  • materia: de tipo String.
  • fecha: LocalDateTime, ahora LocalDateTime.
  • participantes: de tipo List de String con los nombres de los estudiantes.

Para que la fecha la guarde en formato LocalDateTime, no Date.

Para que la serialización/deserialización funcione correctamente, debes crear una clase que herede la clase TypeAdapter:

public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime>;

Emplea el formato siguiente para la fecha en la serialización del objeto de tipo LocalDateTime:

private static final DateTimeFormatter formato 
          = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

Crea una sencilla aplicación que cree un examen de “Acceso a Datos” para el 12 de noviembre del 2023 a las 9:45 horas, con 5 estudiantes con nombres de poetas femeninas del siglo XX.

Guarda el examen en una archivo JSON llamado accesoADatos.json (de manera “vistosa” y con formato de fecha anterior mediante el api de Gson y muestre el contenido del archivo por pantalla, utilizando Files de Java NIO.2 y recupere el archivo para guardarlo en un nuevo objeto Java.

Ayuda:

Ejercicios con TypeAdapter

Ejercicio 1: Serialización y deserialización básica Serializar y deserializar una clase sencilla con atributos básicos.

Crea una clase Persona con atributos nombre y edad. Implementa un TypeAdapter para esta clase, personalizando los nombres de los atributos en el JSON resultante, de modo que aparezcan como name y age en formato JSON.

Ejercicio 2: Serialización y deserialización de objetos anidados

Manejar una clase que contiene otro objeto como atributo.

Crea una clase Direccion con atributos calle y ciudad. Añade un atributo de tipo Direccion. Implementa los adaptadores de tipo necesarios para manejar esta estructura de modo que la dirección tenga el nombre address y aparezca como una cadena de texto con el formato calle, ciudad.

Ejercicio 3: Serialización y deserialización de listas

Manejar una clase que contiene una lista de objetos.

Añade a la clase Persona una lista de objetos Persona llamados amigos. Implementa los adaptadores de tipo para manejar la lista de amigos en el JSON. Haz que la lista de amigos la represente como un array de objetos JSON.

Ejercicio 4: Serialización y deserialización de números personalizados

Personalizar la serialización y deserialización de números.

Crea una clase Producto con atributos nombre y precio. Implementa un TypeAdapter que formatee el precio como una cadena con dos decimales en el JSON.

Ejercicio 5: Serialización y deserialización de arrays

Manejar una clase que contiene un array de objetos.

Añade a la clase persona un atributo hobbies. Implementa los adaptadores de tipo para manejar el array de hobbies en el JSON para que aparezca como una lista de cadenas de texto separadas por guion.

Última actualización: 23.09.2025

01.07 Gson. JsonReader


1. La clase JsonReader

La clase JsonReader de GSON es el analizador JSON en streaming de GSON.

Un JsonReader permite leer una cadena JSON o un archivo como una secuencia de tokens JSON, JsonToken.

Iterar token por token en JSON también se conoce como streaming o flujo a través de los tokens JSON. Así, a veces se hace referencia al JsonReader de GSON como un analizador JSON en streaming.

Un flujo incluye:

  • Elementos literales: cadenas, números, booleanos y nulos.
  • Delimitadores de inicio y fin de objetos y arrays ({, }, [, ]).

Los tokens de JSON se recorren en profundidad, en el mismo orden en que aparecen en el documento JSON.

Los Objetos JSON, los pares nombre/valor se representan en un único Token.

Los analizadores en streaming suelen implementarse en dos versiones:

  • Analizadores de extracción (pull parser): analizador en el que el código que lo utiliza extrae los tokens del analizador cuando está listo para gestionar el siguiente token.
  • Analizadores de empuje (push parser): un push parser analiza los tokens JSON y los envía a un gestor de eventos.

JsonReader de GSON es un pull parser.

1.1 Creación de un JsonReader

Se puede crear un JsonReader de GSON por medio de su constructor (único).
El constructor del JsonReader recoge un Reader Java como argumento:

public JsonReader (Reader in);

Por ejemplo:

String json = "{\"nome\" : \"Alejandra Pizarnik\", \"idade\" : 36}";

JsonReader jsonReader = new JsonReader(new StringReader(json));

En el ejemplo anterior se lee de un flujo de cadena de tipo StringReader, pasándole el objeto de tipo StringReaderal constructor del JsonReader.
El StringReader es un flujo de tipo Reader que convierte una cadena Java en una secuencia de caracteres (es decir, un Reader).

Readers Java

Nota: recuerda el tipo de Readers que existen en Java. Entre otros: BufferedReader (y subclase LineNumberReader), CharArrayReader, FilterReader (subclase PushbackReader), InputStreamReader (y subclase FileReader), PipedReader o StringReader

Diagrama de clases de Readers Java:

Diagrama de clases java.io.Reader Diagrama de clases java.io.Reader

2. Iteración de los Tokens JSON JsonToken de un JsonReader

Una vez creada una instancia de JsonReader, se puede iterar a través de los tokens JSON que lee del Reader pasado al constructor del JsonReader.

La clase JsonToken tiene constantes de enumeración para identificar el tipo de token:

Constante enumeración Descripción
BEGIN_ARRAY Apertura de un array JSON.
BEGIN_OBJECT Apertura de un objeto JSON.
BOOLEAN Valor JSON true o false.
END_ARRAY Cierre de un array JSON.
END_DOCUMENT Final del flujo JSON.
END_OBJECT Cierre de un objeto JSON.
NAME Nombre de una propiedad JSON.
NULL Valor JSON nulo.
NUMBER Número JSON representado por un double, long o int en Java.
STRING String JSON.

Para acceder a los tokens del JsonReader, se puede utilizar un bucle similar al siguiente:

while (jsonReader.hasNext()) {

}

El método hasNext() del JsonReader devuelve true si el tiene más tokens.

String json = "{\"nome\" : \"Alejandra Pizarnik\", \"idade\" : 36}";

JsonReader jsonReader = new JsonReader(new StringReader(json));

try {
    while (jsonReader.hasNext()) {
        JsonToken siguienteToken = jsonReader.peek(); // devuelve el siguiente, sin consumirlo.
        System.out.println(siguienteToken);

        if (JsonToken.BEGIN_OBJECT == siguienteToken) {
            // Si es un objeto, consumimos las llaves {
            jsonReader.beginObject();

        } else if (JsonToken.NAME == siguienteToken) {
            // Si es un nombre de atributo, lo imprimimos.
            String nomeAtributo = jsonReader.nextName();
            System.out.println(nomeAtributo);

        } else if (JsonToken.STRING == siguienteToken) {
            // si es una cadena, recuperamos String y la imprimimos
            String valorString = jsonReader.nextString();
            System.out.println(valorString);

        } else if (JsonToken.NUMBER == siguienteToken) {
            // Si es un número, OJO con los tipos...
            long valorNumero = jsonReader.nextLong();
            System.out.println(valorNumero);

        }
    }
} catch (IOException e) {
    System.err.println(e.getMessage());
}

También podría haberse hecho con un switch:

String json = "{\"nome\" : \"Alejandra Pizarnik\", \"idade\" : 36}";

JsonReader jsonReader = new JsonReader(new StringReader(json));

while (jsonReader.hasNext()) {
    JsonToken siguienteToken = jsonReader.peek(); // devuelve el siguiente, sin consumirlo.
    System.out.println(siguienteToken);

    if (null != siguienteToken) {
        switch (siguienteToken) {
            case BEGIN_OBJECT -> // Si es un objeto, consumimos las llaves {
                jsonReader.beginObject();
            case NAME -> {
                // Si es un nombre de atributo, lo imprimimos.
                String nomeAtributo = jsonReader.nextName();
                System.out.println(nomeAtributo);
            }
            case STRING -> {
                // si es una cadena, recuperamos String y la imprimimos
                String valorString = jsonReader.nextString();
                System.out.println(valorString);
            }
            case NUMBER -> {
                // Si es un número, OJO con los tipos...
                long valorNumero = jsonReader.nextLong();
                System.out.println(valorNumero);
            }
            default -> {
            }
        }
    }
}

El método peek() del JsonReader devuelve el siguiente token JSON, pero sin moverse sobre él (sin devolver el siguiente). Sucesivas llamadas a peek() devolverán el mismo token JSON.

El JsonToken devuelto por peek() se puede comparar con constantes en la clase JsonToken para averiguar qué tipo de token es. Puedes ver cómo se hace esto en el bucle del código anterior.

Dentro de cada declaración if, se llama a un método del JsonReader, next _TipoDato_ () , lee del JsonReader el token actual y avanza al siguiente.
Todos los métodos beginObject(), nextString() y nextLong() devuelven el valor del token actual y mueven el puntero interno al siguiente Token.

3. “Parser” personalizado de JSON con JsonReader

Para analizar (“parsear”) un flujo JSON por medio de un JsonReader mediante un parser descendente recursivo:

a) Creamos un método inicial que cree un JsonReader y llame a un método de lectura de un array de objetos JSON. Finalmente, cierra el JsonReader: - El método principal de entrada crea un JsonReader a partir de un InputStream (que convertiremos en un Reader) o un Reader. - Llama a un método de lectura de los tokens JSON de un array o de objetos JSON. - Finalmente, cierra el JsonReader.

import com.google.gson.stream.JsonReader;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;

public class PoemaJsonReader {

    // Método principal de entrada
    public List<Poema> readJsonStream(InputStream in) throws IOException {
        // JsonReader necesita un Reader, por lo que convertimos el InputStream en un InputStreamReader
        // Además, implanta la interfaz Closeable, por lo que podemos cerrarlo en un bloque try-with-resources
        try(JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"))) {
            return readArrayPoemas(reader);
        }
    }
}

b) Creamos métodos de gestión/control para cada estructura del objeto JSON. Se necesita un método para cada tipo de objeto y para cada tipo de array:

  • Dentro de los métodos de gestión de arrays, primero llamamos a beginArray() para consumir el corchete de apertura del array. Luego, se crea un bucle while que acumula valores, terminando cuando hasNext() sea false. Finalmente, se lee el corchete de cierre del array llamando a endArray().

  • Dentro de los métodos de gestión de objetos, primero se invoca a beginObject() para consumir la llave de apertura del objeto. Luego, crea un bucle while que asigna valores a variables locales según su nombre. Este bucle debe terminar cuando hasNext() sea false. Finalmente, se lee la llave de cierre del objeto llamando a endObject().

    import com.google.gson.stream.*;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    public class PoemaJsonReader {
    
        // Método principal de entrada
        // ...
        /**
         * Ejemplo que lee un array de objetos JSON
         * Devuelve la lista de poemas del JSON
         * */
        public List<Poema> readArrayPoemas(JsonReader reader) throws IOException {
            // Guardar la lista de poemas del JSON
            List<Poema> poemas = new ArrayList<>();
    
            reader.beginArray(); // Leemos el  [
            while (reader.hasNext()) { // para cada elemento de array de poemas
                poemas.add(readPoema(reader));
            }
            reader.endArray(); // Leemos el ]
            return poemas;
        }
    
        public Poema readPoema(JsonReader reader) throws IOException {
            // Código de lectura de un poema
        }
    
    }

Cuando se encuentra un objeto o array anidado, delega al método de control correspondiente.

Cuando se encuentra un nombre desconocido, los analizadores estrictos deberían fallar con una excepción. Los analizadores permisivos deben llamar a skipValue() para omitir de forma recursiva los tokens anidados del valor, que de lo contrario podrían entrar en conflicto.

Si un valor puede ser nulo, debes verificar primero utilizando peek(). Los literales nulos se pueden consumir utilizando nextNull() o skipValue().

[
  {
    "id": 123456789012,
    "poema": "I dwell in Possibility",
    "localizacion": null,
    "poeta": {
      "nome": "Emily Dickinson",
      "anoNacemento": 1830,
      "numeroSeguidores": 150
    }
  },
  {
    "id": 123456789013,
    "poema": "Still I Rise",
    "localizacion": [34.0522, -118.2437],
    "poeta": {
      "nome": "Maya Angelou",
      "anoNacemento": 1928,
      "numeroSeguidores": 300
    }
  }
]

El parser sería algo así:

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class PoemaJsonReader {

    // Método principal de entrada
    public List<Poema> readJsonStream(InputStream in) throws IOException {
        JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
        try {
            return readArrayPoemas(reader);
        } finally {
            reader.close();
        }
    }

    /**
     * Devuelve la lista de poemas del JSON
     * */
    public List<Poema> readArrayPoemas(JsonReader reader) throws IOException {
        // Guardar la lista de poemas del JSON
        List<Poema> poemas = new ArrayList<>();

        reader.beginArray(); // Leemos el  [
        while (reader.hasNext()) { // para cade elemento de array de poemas
            poemas.add(readPoema(reader));
        }
        reader.endArray(); // Leemos el ]
        return poemas;
    }

    public Poema readPoema(JsonReader reader) throws IOException {
        long id = -1;
        String poema = null; // el nombre del poema
        Poeta poeta = null; // El poeta es un objeto
        List<Double> localizacion = null; // Es un array JSON de double

        reader.beginObject(); // Lectura  {
        while (reader.hasNext()) { // Mientras haya atributos
            String nome = reader.nextName();
            if (nome.equals("id")) {
                id = reader.nextLong();
            } else if (nome.equals("poema")) {
                poema = reader.nextString();
            } else if (nome.equals("localizacion")
                    // peek devuelve el siguiente elemento sin consumirlo
                    // (no salta al siguiente). Es un array.
                    && reader.peek() != JsonToken.NULL) {
                localizacion = readArrayDouble(reader);
            } else if (nome.equals("poeta")) {
                poeta = readPoeta(reader);
            } else {
                reader.skipValue();
            }
        }
        reader.endObject(); // Lectura  {
        return new Poema(id, poema, poeta, localizacion);
    }

    public List<Double> readArrayDouble(JsonReader reader) throws IOException {
        List<Double> doubles = new ArrayList<>();

        reader.beginArray(); // [
        while (reader.hasNext()) {
            doubles.add(reader.nextDouble());
        }
        reader.endArray(); // ]
        return doubles;
    }

    public Poet readPoeta(JsonReader reader) throws IOException {
        String nome = null;
        int anoNacemento = -1;
        int numeroSeguidores = -1;

        reader.beginObject();
        while (reader.hasNext()) {
            String fieldName = reader.nextName();
            if (fieldName.equals("nome")) {
                nome = reader.nextString();
            } else if (fieldName.equals("anoNacemento")) {
                anoNacemento = reader.nextInt();
            } else if (fieldName.equals("numeroSeguidores")) {
                numeroSeguidores = reader.nextInt();
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();
        return new Poet(nome, anoNacemento, numeroSeguidores);
    }
}
Ejercicio. Adaptación de datos de Meteogalicia

Inicialmente haz el ejercicio con JsonDeserializer y luego con JsonReader.

MeteoGalicia suministra un API JSON para la lectura de datos meteorológicos. Los JSONs de MeteoGalicia pueden consultarse en:

https://www.meteogalicia.gal/web/rss-georss-json

Un ejemplo de predicción a corto plato es:

https://servizos.meteogalicia.gal/mgrss/predicion/jsonPredConcellos.action?idConc=15078&request_locale=gl:

{
    "predConcello": {
        "idConcello": 15078,
        "listaPredDiaConcello": [
            {
                "ceo": {
                    "manha": 108,
                    "noite": 208,
                    "tarde": 107
                },
                "dataPredicion": "2023-10-29T00:00:00",
                "nivelAviso": 0,
                "pchoiva": {
                    "manha": 95,
                    "noite": 55,
                    "tarde": 75
                },
                "tMax": 14,
                "tMin": 11,
                "tmaxFranxa": {
                    "manha": 12,
                    "noite": 11,
                    "tarde": 14
                },
                "tminFranxa": {
                    "manha": 11,
                    "noite": 10,
                    "tarde": 11
                },
                "uvMax": -9999,
                "vento": {
                    "manha": 306,
                    "noite": 300,
                    "tarde": 300
                }
            }
        ],
        "nome": "Santiago de Compostela"
    }
}

Así, por ejemplo, la documentación de API de JSON a corto plazo es:

https://meteo-estaticos.xunta.gal/datosred/infoweb/meteo/docs/rss/JSON_Pred_Concello_gl.pdf

En la que pueden consultarse los identificadores de concello y el formato JSON. Para Santiago tenemos:

https://servizos.meteogalicia.gal/mgrss/predicion/jsonPredConcellos.action?idConc=15078&request_locale=gl

Teniendo en cuenta que las varias propiedades identifican un identificador del icono con un número y que el icono está asociado al número por medio de la URL:

https://www.meteogalicia.gal/web/assets/icons/svg/111.svg, siendo 111 el número.

Se pide:

Crea las clases que consideres necesarias para la lectura del objeto JSON.

Mapea los campos se muestren de otro modo: listaPredDiaConcello como prediccionDia, de tipo List.

  • Enumeración con el nombre VariableMeteoroloxica con posibles valores: CIELO, LLUVIA, TEMPERATURA_MAXIMA, TEMPERATURA_MINIMA, VIENTO:
  public enum VariableMeteo {
    CIELO("ceo"), LLUVIA("pchoiva"), TEMPERATURA_MAXIMA("tmaxFranxa"),
    TEMPERATURA_MINIMA("tminFranxa"), VIENTO("vento");

    private String nome;

    VariableMeteo(String nome) {
        this.nome = nome;
    }
    // ... método get para nome
    // Método estático que devuelve la VariableMeteo a partir de un String: 
    // public static VariableMeteo fromString(String nome) {...}
    // Método toString: 
    @Override
    public String toString() {
        return nome;
    }
  }
}
  • VariableFranxa, con dos atributos: variable (de tipo VariableMeteo), valorManha, valorTarde, valorNoche (los tres de tipo entero):
  "tmaxFranxa": {
  "manha": 18,
  "noite": 16,
  "tarde": 20
  },
public class VariableFranxa {
    public static final int NO_DATA = -9999; // Valor por defecto cuando no hay dato para una variable entera.
    
    private VariableMeteoroloxica variableMeteorologica;
    private int valorManha;
    private int valorTarde;
    private int valorNoche;
    
    // Constructores, Getters y Setters
    // ...
    // toString: 

    @Override
    public String toString() {
        return  variable + ": (" + (valorManha!=NO_DATA ? valorManha : "-" ) + ", " + (valorTarde!=NO_DATA ? valorTarde : "-" ) + ", "
                + (valorNoche!=NO_DATA ? valorNoche : "-" )  + ')';
    }
}
  • Concello con idConcello y nome:
public class Concello {
    private int idConcello;
    private String nome;
    
    // Constructores, Getters y Setters
    // ...
    // toString
    @Override
    public String toString() {
        return nome + " [" +idConcello + ']';
    }
}
  • PrediccionDia: dataPrediccion, nivelAviso (int), tMax, tMin, uvMaz, y una List de objetos VariableFranxa con los posibles valores de VariableMeteoroloxia:
public class PrediccionDia {
    rivate LocalDate dataPredicion;// Guádala para que la ponga mejor como LocalDate
    private int nivelAviso;
    private int temperaturaMaxima;
    private int temperaturaMinima;
    private int uvMaximo;
    private List<VariableFranxa> listaVariableFranxa;

    public PrediccionDia() {
        listaVariableFranxa = new ArrayList<>();
    }

    public PrediccionDia(String dataPredicion) {
        this.dataPredicion = LocalDateTime.parse(dataPredicion).toLocalDate();
        listaVariableFranxa = new ArrayList<>();
    }
    // Constructores, Getters y Setters
    // ...
    public void addVariableFranxa(VariableFranxa variableFranxa) {
        listaVariableFranxa.add(variableFranxa);
    }
    // toString
    @Override
    public String toString() {
        return dataPredicion +
                " (aviso: " + nivelAviso + ") " +
                ", Máxima: " + temperaturaMaxima +
                ", Mínima: " + temperaturaMinima +
                ", Índice ultravioleta máx: " + uvMaximo +
                " " + listaVariableFranxa.stream().collect(StringBuilder::new,
                (sb, vf) -> sb.append(vf).append(System.lineSeparator()), StringBuilder::append);
    }
}
  • Prediccion, con atributos: concello de tipo Concello y una lista de valores de PredicciónDia:
public class Prediccion {

    public static final String BARRA = "--------------------------------------------------";

    private Concello concello;
    private List<PrediccionDia> listaPredDiaConcello;

    public Prediccion() {
        listaPredDiaConcello = new ArrayList<>();
    }

    public Prediccion(Concello concello) {
        this.concello = concello;
        listaPredDiaConcello = new ArrayList<>();
    }
    // Constructores, Getters y Setters
// ...
    public void addPredDiaConcello(PrediccionDia predDia){
        listaPredDiaConcello.add(predDia);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Prediccion that = (Prediccion) o;
        return Objects.equals(concello, that.concello);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(concello);
    }

    @Override
    public String toString() {
        return concello + System.lineSeparator() + BARRA + System.lineSeparator()
                + listaPredDiaConcello.stream().collect(StringBuilder::new,
                (sb, pd) -> sb.append(pd).append(System.lineSeparator())
                        .append(BARRA).append(System.lineSeparator()), StringBuilder::append);
    }

a. Crea el modelo de datos de la aplicación, lo más ajustado al diseño anterior.


b. Dada la URL siguiente, haz un programa AppPredicción que lea los datos de la URL:

   public static final String URL = "https://servizos.meteogalicia.gal/mgrss/predicion/" +
            "jsonPredConcellos.action?idConc=15078&request_locale=gl";

Recuerda que con new URI(URL).toURL().openConnection().getInputStream() puedes obtener el flujo de datos de entrada. Debes convertirlo en Reader.


c. Crea un adaptador para ese Json completo, llamado PrediccionDeselializer implements JsonDeserializer<Prediccion> que permita obtener un objeto Java de tipo Prediccion con los datos del Concello, únicamente:

   Santiago de Compostela [15078] 

Haz pruebas con el adaptador para que muestre la información del Concello. Recuerda registrar el adaptador en el objeto GsonBuilder y hacer pruebas con varios concellos.


d. Haz un adaptador para PrediccionDia, public class PrediccionDiaDeserializer implements JsonDeserializer<PrediccionDia>, para que permita adaptar el siguiente JSON, pero solo los atributos de primer nivel, no las variables de franxa (ten en cuenta que los atributos pueden cambiar de nombre y que hay elementos nulos):

    {
        "ceo": {
          "manha": 104,
          "noite": 208,
          "tarde": 108
        },
        "dataPredicion": "2024-11-04T00:00:00",
        "nivelAviso": null,
        "pchoiva": {
          "manha": 5,
          "noite": 70,
          "tarde": 55
        },
        "tMax": 20,
        "tMin": 13,
        "tmaxFranxa": {
          "manha": 18,
          "noite": 16,
          "tarde": 20
        },
        "tminFranxa": {
          "manha": 15,
          "noite": 14,
          "tarde": 16
        },
        "uvMax": 2,
        "vento": {
          "manha": 303,
          "noite": 299,
          "tarde": 305
        }
      }

e. Haz que el adaptador PrediccionAdapter llame al adaptador PrediccionDiaAdapter para que adapte los datos de PrediccionDia.

Ayuda: para que el adaptador de Predicción llame al de PredicciónDia, debes invocar al método deserialize del contexto pasándole el objeto Json y el tipo de objeto a deserializar:

<T> T deserialize (JsonElement json, Type typeOfT) throws JsonParseException
// e es el JsonElement que se pasa al adaptador, en nuestro caso el Json de PrediccionDia que hemos leído en PrediccionAdapter.
pr.addPredDiaConcello(contexto.deserialize(e, PrediccionDia.class));

El resultado debe ser algo así:

Santiago de Compostela [15078]
--------------------------------------------------
2024-11-04 (aviso: 0) , Máxima: 21, Mínima: 11, Índice ultravioleta máx: 2 
--------------------------------------------------
2024-11-05 (aviso: 0) , Máxima: 22, Mínima: 13, Índice ultravioleta máx: 2 
--------------------------------------------------
2024-11-06 (aviso: 0) , Máxima: 22, Mínima: 13, Índice ultravioleta máx: 2 
--------------------------------------------------
2024-11-07 (aviso: 0) , Máxima: 21, Mínima: 12, Índice ultravioleta máx: 2 
--------------------------------------------------

f. Amplía PrediccionDiaDeserializer para que recupere los datos de las variables de franxa.

f.1. Crea un método privado en PrediccionDiaDeserializer llamado getVariableFranxa que recoja el tipo de variable meteorológica y el objeto Json con los valores y devuelva un objeto de tipo VariableFranxa con la siguiente firma:

    private VariableFranxa getVariableFranxa(VariableMeteo v, JsonObject varFranxaJsonObject){
        return null;
    }

En el que el objeto varFranxaJsonObject es el que tiene el formato:

{
  "manha": 18,
  "noite": 16,
  "tarde": 20
},

f.2. Recupera los valores de VariableFranxa del objeto Json de PrediccionDia. Existen varios modos de hacerlo:

  • Recorrer todos los atributos del objeto JSON y, si son de tipo objeto, llamar a un método que los lea o hacerlo en el propio bucle:
    public PrediccionDia deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// ...
        for (Map.Entry<String, JsonElement> entrada : jsonObject.entrySet()) {
            if (entrada.getValue().isJsonObject()) { // ¡ES UNA VARIABLE DE FRANXA!
                entrada.getKey(); // Es el nombre de la variable de franxa, de tipo String
                entrada.getValue().getAsJsonObject(); // Es el objeto JSON de la variable de franxa
            }
        }
// ...
    }
  • Recorrer todos los posibles valores de VariableMeteo y obtener el objeto asociado a ese valor:
    public PrediccionDia deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// ...
        for (VariableMeteo variable : VariableMeteo.values()) {
            if (jsonObject.has(variable.getNome())) { // tiene esa variable de franxa
                getVariableFranxa(variable, jsonObject.get(variable.getNome()).getAsJsonObject());
            }
        }
// ...
    }

f.3. Modifica el método toString() de VariableMeteo para que imprima según la siguiente regla:

CIELO -> "Cielo";
LLUVIA -> "Lluvia";
TEMPERATURA_MAXIMA -> "Tempertura máxima";
TEMPERATURA_MINIMA -> "Temperatura mínima";
VIENTO -> "Viento";

El resultado tras la ejecución debe ser algo así:

Santiago de Compostela [15078]
--------------------------------------------------
2024-11-06 (aviso: 0) , Máxima: 23, Mínima: 16, Índice ultravioleta máx: 2 
Cielo: (103, 103, 202)
Lluvia: (5, 5, 5)
Tempertura máxima: (21, 23, 18)
Temperatura mínima: (16, 19, 14)
Viento: (300, 306, 302)
--------------------------------------------------
2024-11-07 (aviso: 0) , Máxima: 21, Mínima: 14, Índice ultravioleta máx: 2 
Cielo: (102, 102, 204)
Lluvia: (5, 5, 5)
Tempertura máxima: (20, 21, 17)
Temperatura mínima: (14, 18, 13)
Viento: (304, 300, 299)
--------------------------------------------------
2024-11-08 (aviso: 0) , Máxima: 18, Mínima: 11, Índice ultravioleta máx: 2 
Cielo: (104, 108, 205)
Lluvia: (5, 55, 35)
Tempertura máxima: (-, -, -)
Temperatura mínima: (-, -, -)
Viento: (300, 306, 301)
--------------------------------------------------
2024-11-09 (aviso: 0) , Máxima: 17, Mínima: 9, Índice ultravioleta máx: 2 
Cielo: (104, 104, 205)
Lluvia: (10, 10, 5)
Tempertura máxima: (-, -, -)
Temperatura mínima: (-, -, -)
Viento: (299, 299, 299)
--------------------------------------------------

g. Recuperación de la lista de Concellos de MeteoGalicia. El json con la lista de concellos está compartido en “común”: concellos.json, y tiene el siguiente formato:

[
  {
    "id": 15078,
    "nombre": "Santiago de Compostela"
  },
  {
    "id": 15079,
    "nombre": "A Coruña"
  },
  {
    "id": 15080,
    "nombre": "Ferrol"
  },
  {
    "id": 15081,
    "nombre": "Lugo"
  },
  {
    "id": 15082,
    "nombre": "Ourense"
  },
  {
    "id": 15083,
    "nombre": "Pontevedra"
  },
  {
    "id": 15084,
    "nombre": "Vigo"
  }
]

Haz un adaptador para recuperar la lista de concellos. Crea una clase ConcelloAdapter que permita recuperar la lista de concellos de MeteoGalicia. Este adaptador debe ser de tipo TypeAdapter<List<Concello>> y debe devolver una lista de objetos de tipo Concello.

Haz un programa que muestre la lista de concellos.


h. Dado el archivo Json con los concellos clasificados por Provincia, crea una clase Provincia, que tenga un atributo nombre y una lista de concellos. Crea un adaptador para recuperar la lista de provincias y los Concellos asociados.

EL formato del archivo Json es el siguiente:

{
  "A Coruña": [
    {
      "id": 15001,
      "nombre": "Abegondo"
    },
    {
      "id": 15002,
      "nombre": "Ames"
    }
  ],
  "Lugo": [
    {
      "id": 27001,
      "nombre": "Abadín"
    },
    {
      "id": 27002,
      "nombre": "Alfoz"
    }
  ],
  "Ourense": [
    {
      "id": 32001,
      "nombre": "Allariz"
    },
    {
      "id": 32002,
      "nombre": "Amoeiro"
    }
  ],
  "Pontevedra": [
    {
      "id": 36001,
      "nombre": "A Cañiza"
    },
    {
      "id": 36002,
      "nombre": "A Estrada"
    }
  ]
}

i. Dada la interface DAO:

import java.util.List;

public interface Dao <T, K> {
    T getById(K id);
    T getByName(String name);
    List<T> getAll();
}

a) Crea una clase ConcelloDao que implemente la interface Dao<Concello, Integer>.:

  • Debe tener dos atributos estáticos:
    • CONCELLOS_JSON con el nombre del archivo JSON con la lista de concellos (concellos.json).
    • TIPO a emplear en las listas de concellos: new TypeToken<List<Concello>>() {}.getType().
  • La clase ConcelloDao debe tener un atributo concellos de List<Concello> con la lista de concellos.
  • Declara dos constructores:
    • No recoja nada y obtenga la lista de concellos del archivo JSON por defecto, CONCELLOS_JSON.
    • Un constructor que recoja un InputStream/Reader al archivo JSON y cargue la lista de concellos de ese recurso.
  • Los métodos implantados de la interfaz DAO son:
    • getById(Integer idConcello) y getByName(String nomeConcello).
    • getAll().

a) Crea una clase ProvinciaDao que implemente la interfaz Dao<Provincia, Integer>.:

  • Debe tener dos atributos estáticos:
    • PROVINCIAS_FILE con el nombre del archivo JSON con la lista de provincias (provincias.json).
    • TIPO a emplear en las listas de provincias: new TypeToken<List<Provincia>>() {}.getType().
  • La clase ProvinciaDao debe tener un atributo provincias de List<Provincia> con la lista de provincias.
  • Declara dos constructores:
    • Uno que no recoja nada y obtenga la lista de provincias del archivo JSON por defecto, PROVINCIAS_FILE.
    • Un constructor que recoja un InputStream/Reader al archivo JSON y cargue la lista de provincias de ese recurso.
  • Los métodos implantados de la interfaz DAO son:
    • getById(Integer idProvincia) y getByName(String nomeProvincia).
    • getAll().

c) Crea una clase PrediccionDao que implemente la interfaz Dao<Prediccion, Integer>:

  • Debe tener dos atributos estáticos:
    • BASE_URL con la ruta base para la predicción de un concello: "https://servizos.meteogalicia.gal/mgrss/predicion/jsonPredConcellos.action?idConc="
  • Atributos:
    • gson de tipo Gson para la deserialización de los objetos JSON.
    • concelloDao de tipo ConcelloDao para la recuperación de los id de los concellos a partir del nombre o la lista de concellos.
  • Constructores:
    • Uno que no recoja nada y cree un ConcelloDao por defecto, así como un Gson con el adaptador de Prediccion.
    • Un constructor que recoja un ConcelloDao y cree Gson para la deserialización de los objetos JSON con el adaptador de Prediccion.
    • Un constructor que recoja un ConcelloDao y un Gson para la deserialización de los objetos JSON.
  • Implantación de los métodos de la interfaz DAO:
    • getById(Integer idConcello) que recupera la predicción de un concello a partir de su id.
    • getByName(String nomeConcello) que recupera la predicción de un concello a partir de su nombre.
    • getAll() que recupera la predicción de todos los concellos.

j. Mejora el programa AppPredicción para que pueda mostrar la predicción de cualquier concello. Para ello, deberás solicitar al usuario el id, comprobar que existe y mostrar la predicción de ese concello. Haz un menú que permita al usuario buscar el nombre de un concello de modo que contenda la cadena escrita, mostrando los ids de esos concellos. Después, debe permitir al usuario introducir el id del concello y mostrar la predicción.

Última actualización: 23.09.2025

01.08 Gson. Renombrar atributos


1. Introducción

Muchas veces los nombres de los atributos de los objetos JSON no coinciden con los de la clase Java, bien porque es una fuente externa, porque está compartido por otras aplicaciones o porque la clase ya está compilada y no tenemos acceso al código fuente.

Existen varias formas de mapear los atributos JSON a los atributos de una clase Java:

Reglas de nombrado

También puede establecer una política de nomenclatura diferente utilizando la clase GsonBuilder: GsonBuilder.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy) (IDENTITY, UPPER_CAMEL_CASE,…) para el formato de los atributos JSON, asignando un valor de la enumeración:

  • IDENTITY: con esta política de nomenclatura, el nombre del atributo no cambia.
  • LOWER_CASE_WITH_DASHES: modifica el nombre del atributo Java del formato CamelCase a un nombre de atributo en minúsculas donde cada palabra está separada por un guión (-).
  • LOWER_CASE_WITH_UNDERSCORES : modifica el nombre del atributo Java del formato en CamelCase a un nombre de atributo en minúsculas donde cada palabra está separada por un guión bajo (_)
  • UPPER_CAMEL_CASE : asegura que la primera “letra” del nombre del atributo Java esté en mayúscula cuando se serialice en su formato JSON.
  • UPPER_CAMEL_CASE_WITH_SPACES : garantiza de que la primera “letra” del nombre del atributo Java esté en mayúscula cuando se serialice en su formato JSON y que las palabras estén separadas por un espacio.

Pregunta: ¿Por qué no está CamelCase, únicamente? Porque espero que hayas empleado la nomenclatura estándar. ¿Verdad?

2. La anotación @SerializedName

https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html

  • Esta anotación indica que este miembro debe ser serializado a JSON con el valor proporcionado como su nombre de atributo.
  • El uso de esta anotación anulará cualquier FieldNamingPolicy, incluida la política de nomenclatura de campo predeterminada, que pueda haberse establecido en la instancia Gson.
  • También se puede establecer una política de nomenclatura diferente utilizando la clase GsonBuilder: GsonBuilder.setFieldNamingPolicy(com.google.gson.FieldNamingPolicy) para obtener más información.

Por ejemplo:

public class Poeta {
   @SerializedName("nomePoeta") String nome;
   @SerializedName(value="idadePoeta", alternate={"idadePoeta2", "idadePoeta3"}) int idade;
   String c; // Otro atributo

   public Poeta(String nome, String idade, String c) {
     this.nome = nome;
     this.idade = idade;
     this.c = c;
   }
}

La salida generada al serializar una instancia de la clase Poeta:

Poeta poeta = new Poeta("Alejandra Pizarnik", 36, "La vida es dura");
Gson gson = new Gson();
String json = gson.toJson(poeta);
System.out.println(json);

Salida:

{"nomePoeta":"Alejandra Pizarnik","idadePoeta":36,"c":"La vida es dura"}

NOTA: el valor que se especifique en esta anotación debe ser un nombre de campo JSON válido.

Al deserializar, todos los valores especificados en la anotación se deserializarán en el atributo. Por ejemplo el mapeado de la edad tiene múltiples nombres:

Poeta poeta = gson.fromJson("{'idadePoeta':36}", Poeta.class);
Assert.assertEquals(36, poeta.idade);
poeta = gson.fromJson("{'idadePoeta2':25}", Poeta.class);
Assert.assertEquals(25, poeta.idade);
poeta = gson.fromJson("{'idadePoeta3':35}", Poeta.class);
Assert.assertEquals(35, poeta.idade);

import org.junit.Assert; para aserciciones.

Ten en cuenta que Poeta.idade se deserializa ahora desde cualquiera de los campos idadePoeta, idadePoeta2 o idadePoeta3.

3. Estrategias de nombrado: FieldNamingStrategy

La interface FieldNamingStrategy es otra opción que tenemos en Gson para personalizar cómo se deben convertir los nombres de los atributos. Nos permite definir una estrategia propia para convertir los nombres de los campos en JSON.

Se trata de una interface funcional con un único método, por lo que es muy útil el uso de expresiones lambda:

public String translateName(Field f);

Es importante que invoquemos al método setFieldNamingStrategy de GsonBuilder para que tenga efecto.

Por ejemplo:

import com.google.gson.*;

public class AppEjemploEstrategia {
    public static void main(String[] args) {
        Gson gson = new GsonBuilder()
                .setFieldNamingStrategy(new EstrategiaNombres()) // Mejor con lambda
                .create();

        Poeta poeta = new Poeta("Alejandra Pizarnik", 25);

        // Serialización
        String jsonPoeta = gson.toJson(poeta);
        System.out.println("Serializado: " + jsonPoeta);

        // Deserialización
        Poeta poetaDeserializado = gson.fromJson(jsonPoeta, Poeta.class);
        System.out.println("Deserializado: " + poetaDeserializado);
    }

    static class Poeta {
        private String nome;
        private int idade;

        public Poeta(String nome, int idade) {
            this.nome = nome;
            this.idade = idade;
        }

        @Override
        public String toString() {
            return "Poeta {" +
                    "nome='" + nome + '\'' +
                    ", idade=" + idade +
                    '}';
        }
    }

    static class EstrategiaNombres implements FieldNamingStrategy {
        @Override
        public String translateName(Field f) {
            // Personaliza cómo se deben convertir los nombres de los atributos
            if (f.getName().equals("nome")) {
                return "nombre";
            } else {
                return f.getName();
            }
        }
    }
}
Clase java.lang.reflect.Field

La clase Field, es del API de Java, java.lang.reflect.Field, y proporciona información y acceso dinámico a un único campo de una clase o interfaz.
Además de getName(), tiene otros métodos get como; getType(), para obtener la clase tipo; getChar(), getDouble(); etc. También tiene métodos set para todos los tipos de datos, entre otros.

La ventaja de usar FieldNamingStrategy es que es más general y se aplica a todos los atributos en cualquier clase, mientras que los adaptadores personalizados son específicos para una clase en particular.

4. Uso de adaptadores personalizados (TypeAdapter)

Hemos visto que la clase GsonBuilder dispone de un método:

public GsonBuilder registerTypeAdapter (Type type, Object typeAdapter)

Empleando este método nos permite otra forma flexible de mapear los atributos JSON a los atributos de una clase Java sin depender únicamente de la anotación @SerializedName, mediante el uso de adaptadores personalizados y estrategias de serialización/deserialización en GSON.

Se peuden crear adaptadores personalizados implantando las interfaces JsonSerializer y JsonDeserializer para proporcionar una lógica personalizada de cómo se deben serializar y deserializar los campos.

Ejemplo:

import com.google.gson.*;

public class AppAdaptadorNombres {
    public static void main(String[] args) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(Poeta.class, new AdaptadorNombres())
                // Podemos usar lambda para cada adaptador
                .create();

        // Serialización
        Poeta poeta = new Poeta("Elizabeth Bishop", 268);
        String jsonPoeta = gson.toJson(poeta);
        System.out.println("Serializado: " + jsonPoeta);

        // Deserialización
        Poeta poetaDeserializado = gson.fromJson(jsonPoeta, Poeta.class);
        System.out.println("Deserialized: " + poetaDeserializado);
    }

    static class Poeta {
        private String nome;
        private int idade;

        public Poeta(String nome, int idade) {
            this.nome = nome;
            this.idade = idade;
        }

        @Override
        public String toString() {
            return "Poeta {" +
                    "nome='" + nome + '\'' +
                    ", idade=" + idade +
                    '}';
        }
    }

    static class AdaptadorNombres implements JsonSerializer<Poeta>, JsonDeserializer<Poeta> {
        @Override
        public JsonElement serialize(Poeta src, Type typeOfSrc, JsonSerializationContext context) {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("nombre", src.getName());
            jsonObject.addProperty("edad", src.getAge());
            return jsonObject;
        }

        @Override
        public Poeta deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                throws JsonParseException {
            JsonObject jsonObject = json.getAsJsonObject();
            String nome = jsonObject.get("nombre").getAsString();
            int idade = jsonObject.get("edad").getAsInt();
            return new Poeta(nome, idade);
        }
    }
}

En este ejemplo, el adaptador personalizado AdaptadorNombres controla cómo se deben serializar y deserializar los campos de Poeta.

La ventaja con respecto al anterior, es que podemos, fácilmente, adaptar de manera distinta cada clase Java.

Ejercicio. Búsqueda de códigos postales

Existen muchas API libres o de código abierto en Internet. Una de las más curiosas es la que devuelve la localización a la que pertenece un código postal:

https://www.zippopotam.us/

Que está disponible para muchos países, entre ellos España:

El formato del JSON es el siguiente:

{
   "post code": "15705",
   "country": "Spain",
   "country abbreviation": "ES",
   "places": [
       {
           "place name": "Santiago de Compostela",
           "longitude": "-8.5459",
           "state": "Galicia",
           "state abbreviation": "GA",
           "latitude": "42.8782"
       }
   ]
}

1. Crea las clases del modelo Java necesarias para la conversión a archivos Json: Lugar y CodigoPostal.
Emplea estándares de nombres y conversión de tipos de datos (los números no deben representarse como cadenas de texto). Usa nombres significativos en gallego/castellano, como consideres. Sobrescribe los métodos toString, equals y hashCode. Ten en cuenta que el código postal puede hacer referencia a varios lugares y que un lugar solo puede tener un único código postal:

public class Lugar {

    private String nome;
    private double longitud;
    private double latitud;
    private String estado;
    private String abreviaturaEstado;
    // ...

    /**
     * Método que devuelve un String con los datos del lugar en formato HTML con colores.
     * @return String con los datos del lugar en formato HTML con colores.
     */
    public String toHTML() {
        return "<h1>" + nome + "</h1>"
                + "Longitud: " + longitud + "<br/>"
                + "Latitud: " + latitud + "<br/>"
                + "Comunidad: " + estado + "<br/>"
                + "Abreviatura Comunidad: " + abreviaturaEstado + "<br/>";
    }

    /**
     * Método que recoge un boolean si quiero devolver el lugar en formato fila de una tabla HTML.
     * Devuelve un String con los datos del lugar en formato HTML con colores.
     * Si está en una fila de una tabla HTML, el fondo de la fila es de color gris.
     * @param fila boolean que indica si quiero devolver el lugar en formato fila de una tabla HTML.
     */
    public String toHTML(boolean fila) {
        return (fila) ? "<tr style=\"background-color: #cccccc\">"
                + "<td>" + nome + "</td>"
                + "<td>" + longitud + "</td>"
                + "<td>" + latitud + "</td>"
                + "<td>" + estado + "</td>"
                + "<td>" + abreviaturaEstado + "</td>"
                + "</tr>"
                : "<h1>" + nome + "</h1>"
                + "Longitud: " + longitud + "<br/>"
                + "Latitud: " + latitud + "<br/>"
                + "Comunidad: " + estado + "<br/>"
                + "Abreviatura Comunidad: " + abreviaturaEstado + "<br/>";
    }

    /**
     * Método que devuelve un String con los datos del lugar en formato texto.
     * @return String con los datos del lugar en formato texto.
     */
    @Override
    public String toString() {
        return " Lugar: " + nome + System.lineSeparator()
                + " Longitud: " + longitud + System.lineSeparator()
                + " Latitud: " + latitud + System.lineSeparator()
                + " Comunidad: " + estado + System.lineSeparator()
                + " Abreviatura Comunidad: " + abreviaturaEstado + System.lineSeparator();
    }
    //...
    
    
}
public class CodigoPostal {

    private String codigoPostal;
    private String pais;
    private String abreviaturaPais;
    private List<Lugar> lugares;
    //...
    /**
     * Devuelve la lista de lugares como HTML, empleando un forEach para concatenar los lugares.
     * El método forEach recibe un Consumer, que es una interfaz funcional que tiene un método
     * abstracto accept() que recibe un objeto de tipo T y no devuelve nada (void).
     *
     * @return cadena de texto con los lugares en formato HTML
     */
    public String getLugaresAsHTML() {
        StringBuilder sb = new StringBuilder("<html><body>");
        lugares.forEach(lugar -> {
            sb.append(lugar.toHTML()).append("<br>");
        });
        sb.append("</body></html>");
        return sb.toString();
    }

    /**
     * Método que devuelve la lista de lugares como HTML, empleando un forEach para concatenar los lugares.
     * El método forEach recibe un Consumer, que es una interfaz funcional que tiene un método
     * abstracto accept() que recibe un objeto de tipo T y no devuelve nada (void).
     *
     * @param asTable boolean que indica si quiero devolver los lugares en formato fila de una tabla HTML.
     * @return  cadena de texto con los lugares en formato HTML
     */
    public String getLugaresAsHTML(boolean asTable) {
        StringBuilder sb = new StringBuilder("<html><body>");
        if (asTable) {
            sb.append("<table border=\"1\">");
            sb.append("<tr style=\"background-color: #cccccc\">");
            sb.append("<th>Lugar</th>");
            sb.append("<th>Longitud</th>");
            sb.append("<th>Latitud</th>");
            sb.append("<th>Comunidad</th>");
            sb.append("<th>Abreviatura Comunidad</th>");
            sb.append("</tr>");
            lugares.forEach(lugar -> {
                sb.append(lugar.toHTML(true));
            });
            sb.append("</table>");
        } else {
            lugares.forEach(lugar -> {
                sb.append(lugar.toHTML()).append("<br>");
            });
        }
        sb.append("</body></html>");
        return sb.toString();
    }

    @Override
    public String toString() {
        var sb = new StringBuilder("Código Postal: '"
                + codigoPostal + System.lineSeparator()
                + "Pais: '" + pais + System.lineSeparator()
                + "AbreviaturaPais: " + abreviaturaPais + System.lineSeparator());
        lugares.forEach(lugar -> {
            sb.append(lugar).append(System.lineSeparator());
        });
        return sb.toString();
    }
    
    
    
}

2. Crea los adaptadores de para código postal:

public class CodigoPostalDeserializer implements JsonDeserializer<CodigoPostal> {
    //...
}

Y en su versión TypeAdapter, pero en este caso implatan también el método write:

public class CodigoPostalTypeAdapter extends TypeAdapter<CodigoPostal> {
    //...
}
public class LugarDeserializer implements JsonDeserializer<Lugar>{
    //..
}

3. Crea una clase CodigoPostalDAO que implemente la siguiente interfaz:

public interface ICodigoPostalDAO {
    /**
     * Obtiene un objeto CodigoPostal a partir de un código postal.
     * @param codigoPostal Código postal como cadena de texto.
     * @return Objeto CodigoPostal  o null si no se ha podido obtener.
     */
    public CodigoPostal getCodigoPostal(String codigoPostal);

    /**
     * Obtiene un objeto CodigoPostal a partir de un código postal y un país.
     * @param codigoPostal Código postal como cadena de texto.
     * @param pais       País como cadena de texto ("es", "fr", "us", etc.)
     * @return Objeto CodigoPostal  o null si no se ha podido obtener.
     */
    public CodigoPostal getCodigoPostal(String codigoPostal, String pais);
}

4. Crea una aplicación que, dado el código postal, muestre la lista de lugares que corresponden con ese código mediante el patrón Model-View-Controller:

Interfaces de la vista y del controlador:

public interface ICodigoPostalController {

    /**
     * Obtiene la lista de lugaros a partir de un código postal. Si no se encuentra el código postal
     * devuelve null.
     *
     * @param codigoPostal Código postal como cadena de texto
     * @param asHTML       Devuelve los datos en formato HTML
     * @return Lista de lugares como cadena o null si no se ha podido obtener
     */
    public String getLugares(String codigoPostal, boolean asHTML);

    /**
     * Obtiene la lista de lugares a partir de un código postal y un país. Si no se encuentra el código postal
     * devuelve null.
     *
     * @param codigoPostal Código postal como cadena de texto
     * @param pais         País como cadena de texto ("es", "fr", "us", etc.)
     * @param asHTML Devuelve los datos en formato HTML
     * @return  Lista de lugares como cadena o null si no se ha podido obtener
     */
    public String getLugares(String codigoPostal, String pais, boolean asHTML);

    /**
     * Asigna la lista de lugares a la vista a partir de un código postal con el pais por defecto.
     * Si no se encuentra el código postal devuelve null.
     * @param codigoPostal
     * @param asHTML
     */
    public void setLugares(String codigoPostal, boolean asHTML);

    public void setVista(IVistaCodigoPostal vista);


}

Vista:

public interface IVistaCodigoPostal  {
    // Métodos para mostrar los datos al usuario
    /**
     * Muestra un mensaje de error en la vista.
     */
    void mostrarError(String mensaje);

    /**
     * Muestra añade un lugar a la lista de lugares de la vista.
     */
    public void addLugar(String lugar);

    /**
     * Borra la lista de lugares de la vista.
     */
    public void deleteLugares();

    /**
     * Añade un código postal a la lista de códigos postales de la vista.
     */
    public void setLugares(String lugares);

    /**
     * Asigna un controlador a la vista.
     * @param controller Controlador a asignar.
     */
    public void setController(ICodigoPostalController controller);

    /**
     * Muestrea la vista
     */
    public void mostrar();
}

La implementación del controlador debe tener referencias a la vista y al modelo (clase DAO):

public class CodigoPostalController implements ICodigoPostalController {
    // El controlador debe tener una referencia al modelo y a la vista.

    private ICodigoPostalDAO codigoPostalDAO;
    private IVistaCodigoPostal vistaCodigoPostal;

    // Constructor que recoge la referencia al modelo
    public CodigoPostalController(ICodigoPostalDAO codigoPostalDAO, IVistaCodigoPostal vistaCodigoPostal) {
        this.codigoPostalDAO = codigoPostalDAO;
        this.vistaCodigoPostal = vistaCodigoPostal;
    }

    public CodigoPostalController(IVistaCodigoPostal vistaCodigoPostal) {
        codigoPostalDAO = new CodigoPostalDAO();
        this.vistaCodigoPostal = vistaCodigoPostal;
    }

    public CodigoPostalController() {
        codigoPostalDAO = new CodigoPostalDAO();
    }
    //...
}

5.. Diseña una vista de texto y otra en JAva Swing que funcione con este controlador e implante dicha interfaz:

public class VentaCodigoPostal extends JFrame implements IVistaCodigoPostal {
    // Controlador de la vista
    private ICodigoPostalController codigoPostalController;
    // Panel principal de la ventana en la que se muestran los datos.
    private JEditorPane panelDatos; // Por ejemplo
    // Panel principal de la ventana
    private JPanel panelPrincipal; // Por ejemplo
    // ...
}
  1. Haz lo mismo, pero de modo que recoja la localidad (de Galicia o España) y muestre los códigos postales de la misma. Inspecciona el JSON para tomar las decisiones de diseño que consideres oportunas.

https://api.zippopotam.us/es/an/ja%C3%A9n

Referencias de códigos de comunidades: https://www.geonames.org/postalcode-search.html?q=&country=ES&adminCode1=M

Diagrama de clases del proyecto Diagrama de clases del proyecto

Última actualización: 23.09.2025

01.09 Resumen Serialización Gson


1. Introducción

Veamos, a modo de resumen, la serialización de Json utilizando la biblioteca Gson.

En el ejemplos emplearemos la siguiente clase Java:

public class Pelicula {
    private int ano;
    private String titulo;

    // Constructores, getters y setters
}

2. Serializar un Array de objetos

Primero, serialicemos un array de objetos con Gson:

Pelicula[] arrayPelis = {
    new Pelicula(1959, "Los cuatrocientos golpes"), 
    new Pelicula(1937, "La gran ilusión")};
String jsonPelis = new Gson().toJson(arrayPelis);

// El resultado será igual al siguiente String:
String resultado = 
      "[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"},{\"ano\":1937,\"titulo\":\"La gran ilusión\"}]";

3. Serializar una Colección de objetos

Una colección de objetos (List,…):

Collection<Pelicula> listaPelis = 
Lists.newArrayList(new Pelicula(1959, "Los cuatrocientos golpes"), 
        new Pelicula(1937, "La gran ilusión"));
String jsonPelis = new Gson().toJson(listaPelis);

String resultado = 
      "[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"},{\"ano\":1937,\"titulo\":\"La gran ilusión\"}]";

4. Cambio de nombres en Serialización

Podemos cambiar el nombre del campo cuando estamos serializando un objeto (también) con JsonSerializer.

La película, que contiene los campos ano y titulo, los vamos a cambiar en JSON por año y título:

    Pelicula pelicula = new Pelicula(1995, "Seven");
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Pelicula.class, new PeliculaSerializer());
    String jsonString = gsonBuilder.create().toJson(pelicula);

    String expectedResult = "{\"año\":1995,\"título\":\"seven\"}";
    assertEquals(expectedResult, jsonString);
}

Para eso precisamos un serializador personalizado para cambiar el nombre de los atributos:

public class PeliculaSerializer implements JsonSerializer<Pelicula> {
    @Override
    public JsonElement serialize
      (Pelicula pelicula, Type typeOfSrc, JsonSerializationContext context) {
        String otroNombre = "año";
        String otroTitulo = "título";
        // Creamos un nuevo objeto JSON son los nuevos nombres.
        JsonObject jObject = new JsonObject();
        jObject.addProperty(otroNombre, pelicula.getAno());
        jObject.addProperty(otroTitulo, pelicula.getTitulo());

        return jObject;
    }
}

5. Evitar campos en la serialización

También podemos ignorar atributos al serializar un objeto:

Pelicula pelicula = new Pelicula(1995, "Seven");
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Pelicula.class, new SerializadorIgnorarCampos());
String cadenaJson = gsonBuilder.create().toJson(pelicula);

String resultadoEsperado = "{\"ano\":1995}";

También podemos utilizar un serializador personalizado:

public class SerializadorIgnorarCampos implements JsonSerializer<Pelicula> {
    @Override
    public JsonElement serialize
      (Pelicula pelicula, Type typeOfSrc, JsonSerializationContext context) {
        String ano = "ano";
        JsonObject jObject = new JsonObject();
        jObject.addProperty(ano, pelicula.getAno());

        return jObject;
    }
}

También ten en cuenta que probablemente necesitemos hacer esto en casos donde no podemos cambiar el código fuente de la clase, o si el campo debe ignorarse en casos muy concretos. De lo contrario, podemos ignorar el campo más fácilmente con una anotación directa en la clase de entidad.

6. Serializar un campo si cumple con una condición

Un caso más avanzado podría ser si queremos serializar un campo cuando cumple con una condición concreta y personalizada.

Por ejemplo, si queremos serializar el valor entero si es positivo y omitirlo si es negativo:

    Pelicula pelicula = new Pelicula(1996, "Breaking the Waves");
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Pelicula.class, 
      new SerilizadorIgnoraCampoCondicion());
    Gson gson = gsonBuilder.create();
    // empleamos: String toJson (Object src, Type typeOfSrc)
    Type peliculaType = new TypeToken<Pelicula>() {}.getType();
    String jsonPelicula = gson.toJson(pelicula, peliculaType);
    
    String resultado = "{\"titulo\":\"Breaking the Waves\"}";

}

El serializador personalizado sería:

public class SerilizadorIgnoraCampoCondicion 
  implements JsonSerializer<Pelicula> {
    @Override
    public JsonElement serialize
      (Pelicula src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject jsonPelicula = new JsonObject();

        // Criterio: ano >= 0
        if (src.getAno() >= 1990) {
            String ano = "ano";
            jsonPelicula.addProperty(ano, src.getAno());
        }

        String titulo = "titulo";
        jsonPelicula.addProperty(titulo, src.getTitulo());

        return jsonPelicula;
    }
}
Última actualización: 23.09.2025

01.09 Resumen Deserialización Gson


1. Introducción

Veremos las distintas (algunas de ellas) formas de deserializar JSON en objetos Java utilizando Gson.

2. Deserializar JSON a un objeto

El primer ejemplo es deserializar un JSON a un objeto Java por medio del método fromJson. La clase película:

public class Pelicula {
    public int ano;
    public String titulo;

    // + implementaciones estándar de equals y hashCode
}
String json = "{\"ano\":2009,\"titulo\":\"La cinta blanca\"}";

Pelicula pelicula = new Gson().fromJson(json, Pelicula.class);

3. Deserializar JSON con Genérico

Definamos una clase utilizando genéricos:

public class ContenedorGenerico<T> {
    public T valor;
}

Como ejemplo, deserializemos un JSON para el tipo: ContenedorGenerico<Integer>

// Es importante conocer cómo se obtiene el tipo de datos en éste caso.
Type tipoToken = new TypeToken<ContenedorGenerico<Integer>>() { }.getType();
String json = "{\"valor\":8}";
ContenedorGenerico<Integer> enteiro = new Gson().fromJson(json, tipoToken);
// El valor debe coincidir con el Integer 8
// assertEquals(enteiro.valor, new Integer(8));
Obtención del tipo de dato

Como regla general, el tipo de dato si es una clase se nombra como MiClase.class. Sin embargo, en algunas situaciones (con genéricos) dicha expresión es incorrecta. En ese caso debe hacerse así:

  • Crear un TypeToken para la clase concreta y obtener su tipo:
Type tipoToken = new TypeToken<TipoGenerico>() { }.getType();

https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html:

TypeToken en Gson:

*Representa un tipo genérico T. Java aún no proporciona una forma de representar tipos genéricos, así que esta clase lo hace. Obliga a los clientes a crear una subclase de esta clase que permite recuperar la información del tipo incluso en tiempo de ejecución. * Por ejemplo, para crear un literal de tipo para List<String>, puedes crear una clase anónima vacía:

TypeToken<List<String>> list = new TypeToken<List<String>>() {};

Evita capturar una variable de tipo como argumento de tipo de un TypeToken. Debido al borrado de tipo, el tipo de ejecución de una variable de tipo no está disponible para Gson y, por lo tanto, no puede proporcionar la funcionalidad que uno podría esperar, lo que da una falsa sensación de seguridad en tiempo de compilación y puede llevar a una ClassCastException inesperada en tiempo de ejecución.

Si los argumentos de tipo del tipo parametrizado solo están disponibles en tiempo de ejecución, por ejemplo, cuando deseas crear un List<E> basado en un Class<E> que representa el tipo de elemento, se puede utilizar el método getParameterized(Type, Type...).

4. Deserializar JSON atributos adicionales a un objeto

Deserialicemos un JSON complejo que contiene campos adicionales y desconocidos:

String json = "{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\",\"tituloOriginal\":\"Les quatre cents coups\",\"valoracion\":9.9}";
Pelicula pelicula = new Gson().fromJson(json, Pelicula.class);
// pelicula.ano valdrá 1959
// pelicula.titulo "Los cuatrocientos golpes"
// assertEquals(pelicula.ano, 1959);
// assertEquals(pelicula.titulo, "Los cuatrocientos golpes");

Gson ignora los campos desconocidos y simplemente recupera los campos que sepa.

5. Deserializar JSON con nombres de atributos no coincidentes (registerTypeAdapter)

Como hemos planteado en algún ejercicio, a veces JSON que contiene campos no coinciden con los atributos del objeto:

String json = "{\"anoPelicula\":1959,\"tituloPelicula\":\"Los cuatrocientos golpes\"}";
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Pelicula.class, new CambiaNombresDeserializer());
Pelicula pelicula = gsonBuilder.create().fromJson(json, Pelicula.class);
// pelicula.ano valdrá 1959
// pelicula.titulo "Los cuatrocientos golpes"
// assertEquals(pelicula.ano, 1959);
// assertEquals(pelicula.titulo, "Los cuatrocientos golpes");

El deserializador personalizado debe analizar los atributos de la cadena JSON y asignarlos al objeto Pelicula:

public class CambiaNombresDeserializer implements JsonDeserializer<Pelicula> {

    // El método devuelve la Pelicula
    @Override
    public Pelicula deserialize
      (JsonElement jElement, Type typeOfT, JsonDeserializationContext context) 
      throws JsonParseException {

        // Recogemos los valores del objeto JSON
        JsonObject jObject = jElement.getAsJsonObject();
        int ano = jObject.get("anoPelicula").getAsInt();
        String titulo = jObject.get("tituloPelicula").getAsString();

        // creamos la Pelicula y la devolvemos
        return new Pelicula(ano, titulo);
    }
}

6. Deserializar un array JSON a un array de objetos Java

Por ejemplo, deserializamos un array JSON en un array de objetos Pelicula:

String json = "[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"}," +
      "{\"ano\":1998,\"titulo\":\"Los idiotas\"}]";

Pelicula[] arrayDestino = new GsonBuilder().create().fromJson(json, Pelicula[].class);

// Pruebas:
// assertThat(Lists.newArrayList(arrayDestino), hasItem(new Pelicula(1959, "Los cuatrocientos golpes")));
// assertThat(Lists.newArrayList(arrayDestino), hasItem(new Pelicula(1998, "Los idiotas")));
// assertThat(Lists.newArrayList(arrayDestino), not(hasItem(new Pelicula(1998, "Los cuatrocientos golpes"))));

7. Deserializar un array JSON a una Collection Java (List,…)

Se puede deserializar directamente un array JSON en un objeto de tipo Collection:

String json = "[{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"}," +
      "{\"ano\":1998,\"titulo\":\"Los idiotas\"}]";

// Es el único punto "confluctivo", obtener el tipo de una colección o genérico:
Type tipoClaseDestino = new TypeToken<ArrayList<Pelicula>>() { }.getType();
Collection<Pelicula> coleccion = new Gson().fromJson(json, tipoClaseDestino);

//assertThat(coleccion, instanceOf(ArrayList.class));

8. Deserializar un JSON a objectos anidados

Ahora, definamos una clase anidada, PeliculaConDirector:

public class PeliculaConDirector {
    public int ano;
    public String titulo;
    public Director director;

    public class Director {
        public String nome;
    }
}

Y así es como deserializamos una entrada que contiene este objeto anidado:

String json = "{\"ano\":1959,\"titulo\":\"Los cuatrocientos golpes\"," +
        "\"director\":{\"nome\":\"François Truffaut\"}}";
PeliculaConDirector pelicula = new Gson().fromJson(json, PeliculaConDirector.class);

assertEquals(pelicula.ano, 1959);
assertEquals(pelicula.titulo, "Los cuatrocientos golpes");
assertEquals(pelicula.director.nome, "François Truffaut");

9. Deserializar JSON con un constructor personalizado

Hemos visto cómo declarar un constructor específico durante las deserializaciones en lugar del constructor predeterminado sin argumentos, utilizando InstanceCreator:

public class PeliculaInstanceCreator implements InstanceCreator<Pelicula> {
    @Override
    public Pelicula createInstance(Type type) {
        return new Pelicula("Funny Games");
    }
}

Lo registramos con registerTypeAdapter para la deserialización:

String json = "{\"ano\":1997}";

GsonBuilder gsonBuilder = new GsonBuilder();

gsonBuilder.registerTypeAdapter(Pelicula.class, new PeliculaInstanceCreator());
Pelicula pelicula = gsonBuilder.create().fromJson(json, Pelicula.class);

// assertEquals(pelicula.ano, 1997);
// assertEquals(pelicula.titulo, "Funny Games");

En lugar de null, Pelicula.titulo es igual a “Funny Games” ya que utilizamos el constructor:

public Pelicula(String titulo) {
    this.titulo = titulo;
}
Ejercicio. Joke API

Una API sencilla es la Joke API, en la que puedes consultar entres 1369 chistes, aleatorio o por categoría, así como en varios idiomas:

https://sv443.net/jokeapi/v2/

El formato del JSON de salida es el siguiente:

{
    "error": false,
    "category": "Programming",
    "type": "twopart",
    "setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
    "delivery": "Porque C no las trata como objetos.",
    "flags": {
        "nsfw": false,
        "religious": false,
        "political": false,
        "racist": false,
        "sexist": false,
        "explicit": false
    },
    "safe": true,
    "id": 6,
    "lang": "es"
}

Una posible petición es:

https://v2.jokeapi.dev/joke/Programming?lang=es

Las categorías son: Any (excluyente), Programming, Miscellaneous., Dark, Pun, Spooky, Christmas.

Además, se pueden solicitar varias categorías a la vez (menos Any):

https://v2.jokeapi.dev/joke/Programming,Dark,Christmas?lang=es

Las banderas negras (&blacklistFlags=nsfw) pueden ser: nsfw, religious, political, racist, sexist, explicit:

https://v2.jokeapi.dev/joke/Programming,Christmas?lang=es&blacklistFlags=nsfw

Se pide: a) Crea las clases Java que consideres adecuada para la aplicación, empleando la nomenclatura estándar y guardando las banderas en una enumeración. Los atributos de las clases no tiene que ajustarse a los del archivo JSON.

b) Crea una clase ChisteDAO que obtenga los chistes del API. Al menos debe tener: getChiste(), que devuelve uno aleatorio; getChisteByLang(String Lang); getChisteByCategory(String category).

c) Haz una aplicación con un menú que pida un tipo de chiste y lo muestre por pantalla. Si lo deseas, haz una aplicación gráfica.

Última actualización: 23.09.2025

Ejercicio. Trivial

Se desea realizar una aplicación para gestionar Preguntas de Trivial. La aplicación debe permitir la creación de preguntas de dos tipos elección múltiple y verdadero falso.

Emplearemos como base la estructura de datos del api Open Trivia Database que proporciona preguntas en formato JSON.

A modo de ejemplo, podéis consultar el siguiente JSON:

{
  "response_code": 0,
  "results": [
    {
      "type": "multiple",
      "difficulty": "medium",
      "category": "Science: Computers",
      "question": "What is the correct term for the metal object in between the CPU and the CPU fan within a computer system?",
      "correct_answer": "Heat Sink",
      "incorrect_answers": [
        "CPU Vent",
        "Temperature Decipator",
        "Heat Vent"
      ]
    }
  ]
}

O una de verdadero o falso:

{
  "response_code": 0,
  "results": [
    {
      "type": "boolean",
      "difficulty": "medium",
      "category": "Science: Computers",
      "question": "The programming language 'Python' is based off a modified version of 'JavaScript'.",
      "correct_answer": "False",
      "incorrect_answers": [
        "True"
      ]
    }
  ]
}

Modelo de datos

De momento implanta las opciones como una lista de cadenas en la clase Pregunta.

classDiagram
direction BT
class Pregunta
class PreguntaMultiple
class PreguntaVerdaderoFalso
PreguntaMultiple  -->  Pregunta 
PreguntaMultiple "1" *--> "opciones *" Opcion 
PreguntaVerdaderoFalso  -->  Pregunta 

1. Clase final Opcion

Clase final que representa cada una de las opciones de una pregunta tipo test.

Atributos

  • enunciado: con el enunciado de la opción de la pregunta tipo test.
  • correcta: que indica si es una opción correcta o no.

Constructor

  • Un constructor sin parámetros.
  • Un constructor que recoge el enunciado, marcándola como incorrecta.
  • Un constructor que recoge el enunciado y si es correcta o no.

Métodos (funciones miembro):

  • Sobrescribe toString para que devuelva el enunciado. Si la opción es correcta devuelve el enunciado con un [*] al final de la cadena. Verifica nulos.

3. Clase final Categoria

Clase Categoria con un único atributo llamado nombre que recoja el nombre de la categoría de la pregunta. La clase debe tener:

Atributos

  • Una contante DEFAULT_CATEGORY con el valor “General” que se empleará como categoría por defecto.
  • Un atributo final nombre para el nombre de la categoría.

Constructores

  • Un constructor que recoge el nombre de la categoría.
  • Un constructo por defecto que inicializa el nombre a “Sin categoría”.

Métodos

  • Sobrescribe los métodos equals y hashCode para que dos categorías sean iguales si tienen el mismo nombre.
  • Sobrescribe el método toString para que devuelva el nombre de la categoría.

2. Clase Pregunta

Clase Pregunta implementa la interfaz Comparable<Pregunta> y Serializable. Las preguntas tienen:

  • Identificador de la pregunta, de tipo Long (clase contenedora): idPregunta. Es mutable. En primera instancia no lo vamos a emplear pero es necesario para futuras ampliaciones.

  • TipoPregunta: enumeración con los valores BOOLEAN y MULTIPLE. La enumeración debe tener:

    • Un atributo tipoPregunta que guarde el tipo de pregunta en forma de cadena, que tendrá los valores Verdadero/Falso y Multiple.
    • un método getTipoPregunta() que devuelva el tipo de pregunta.
    • Un método estático que recoja una cadena y devuelva el tipo de pregunta de tipo enumerado: public static TipoPregunta getTipoPregunta(String tipoPregunta)
  • dificultad, de tipo Dificultad: enumeración con los valores EASY, MEDIUM, HARD. La enumeración debe tener:

  • Un atributo dificultad que guarde la dificultad de la pregunta en forma de cadena.

  • un método getDificultad que devuelva la dificultad de la pregunta.

  • Un método estático getDificultad que recoja una cadena y devuelva la dificultad de la pregunta de tipo enumerado.

  • categoria: de tipo Categoria

  • pregunta: enunciado de la pregunta.

Constructor

Dos constructores:

  • Un constructor por defecto.
  • Un constructor que recoge el enunciado de la pregunta.

Los métodos set devolverán una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así, hay que hacerlo de manera explícita, con return this, facilitando la creación de objetos y evitando la necesidad de crear varios constructores con muchos parámetros.

Funciones miembro

A ser posible, los métodos set deben devolver una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así hay que hacerlo de manera explícita, con return this.

  • Método toString que devuelve el número y el enunciado de la pregunta. Con el siguiente formato: número. Enunciado con la primera en mayúscula.
  • Método compareTo que compara dos preguntas por su enunciado, tipo de pregunta, dificultad y categoría. Si el enunciado es igual, se comparará por tipo de pregunta, dificultad y categoría.
  • Sobrescribe los métodos equals y hashCode para que dos preguntas sean iguales si tienen el mismo enunciado, tipo de pregunta, dificultad y categoría (en concordancia con el método compareTo).

3. Clase PreguntaMultiple implanta la interface Predicate<Int>

Clase PreguntaMultiple que hereda de Pregunta e implementa la interfaz Predicate<Int>. Un predicado es una interfaz funcional con un método que devuelve un valor booleano (test). En este caso, la función test devuelve verdadero si el número de la respuesta correcta es igual al número pasado como parámetro. Por ejemplo, si se llama a test(3) y la respuesta correcta a la pregunta es 3, devolverá verdadero:

var pregunta = new PreguntaMultiple("¿Cuál es la capital de España?");
// ...

System.out.println(pregunta.test(3)); // true

Las preguntas multichoice tiene únicamente una lista de tipo Opcion.

Atributos

  • opciones: lista de preguntas, de tipo Opcion.

Constructores

  • Un constructor por defecto que inicializa la lista de preguntas.
  • Uno que recoge la pregunta enunciado, creando la lista.

Funciones miembro

El método set y el método addOpcion/addOpciones deben devolver una referencia al propio objeto para poder concatenar las asignaciones. Si se hace así hay que hacerlo de manera explícita, con return this.

  • addOpcion: recoge una opción (de tipo Opcion) y la añade.
  • addOpciones: recoge una lista de opciones (de tipo Opcion) y las añade a la lista actual.
  • get y set para el atributo opciones.
  • getNumCorrectas: devuelve el número de opciones correctas de la pregunta.
  • public int getPuntos(List<Integer> marcadas): recoge una lista de enteros con los números de las opciones marcadas (pueden marcar varias) y devuelve los puntos obtenidos. Las incorrectas cuentan negativo. Y ten en cuenta que se considera un punto por pregunta correcta.

Para ello, debes “recorrer” la lista de opciones (marcadas) y comprobar si es correcta o no, llevando cuenta de las correctas y las incorrectas: Los puntos se calculan con la fórmula:

var puntos = (marcadasBien-marcadasMal)/numCorrectas;
  • toString: devuelve el enunciado (invoca al toString de la clase padre) y la lista de opciones con el número de opción:
1. ¿Cuál es la capital de España?
    a. Madrid
    b. Barcelona
    c. Sevilla
    d. Valencia

Emplea la clase StringBuilder para crear la cadena o una estrategia lo más eficiente posible.

  • test: implantación del método test de la interfaz. Recoge el Integer y devuelve verdadero si la opción seleccionada es correcta. Comprueba que el valor recogido es un valor válido entre 0 y el número de opciones, además de comprobar que esa opción no es nula (obviamente, en Kotlin esa verificación no es precisa y se realiza de manera más sencilla con el operador ?.)

4. Clase PreguntaVerdaderoFalso

Clase PreguntaVerdaderoFalso que hereda de Pregunta e implementa la interfaz Predicate<Boolean>.

Las preguntas verdadero/falso sólo tiene un booleano que indica si la respuesta es verdadera o falsa.

Atributos

  • respuesta: booleano que indica si la respuesta es verdadera o falsa.

Constructores

  • Un constructor por defecto.
  • Un constructor que recoge el enunciado de la pregunta.
  • Un constructor que recoge el enunciado de la pregunta y si es correcta o no.

Métodos

  • toString: devuelve el enunciado (invoca al toString de la clase padre) con las opciones verdadero y falso, marcando la correcta con un asterisco al final de la cadena.
1. ¿La capital de España es Madrid?
    a. Verdadero [*]
    b. Falso
  • test: implantación del método test de la interfaz. Recoge el Boolean y devuelve verdadero si la opción seleccionada es correcta.

5. Clase AppTrivial

Esta clase debe crear varias preguntas de trivial y mostrarlas por pantalla.

1. ¿Cuál el pais más extenso del mundo?
    a. Rusia
    b. Canadá
    c. China
    d. Estados Unidos
   
2. ¿Es Kotlin un lenguaje de programación?
    a. Verdadero
    b. Falso

Conversión a JSON

Emplea la librería Gson para convertir las preguntas a JSON y viceversa, tanto en cadenas como en ficheros. Hazlo con preguntas tipo test y con preguntas verdadero/falso.

Estudia el resultado mostrado. ¿Es coherente con lo esperado?

Adaptadores de tipo personalizados

1. JsonSerializer

a. Crear un adaptador de tipo personalizado para la enumeración TipoPregunta que ponga el tipo de pregunta como una cadena en minúsculas dentro del objeto JSON de Pregunta. Por ejemplo, si el tipo de pregunta es MULTIPLE, el JSON resultante sería:

{
  "tipoPregunta": "multiple"
}

Hazlo con expresiones lambda y con una clase anónima.

b. Implementa un adaptador de tipo personalizado para la enumeración TipoPregunta que convierta el tipo de pregunta a un objeto JSON con el siguiente formato:

{
  "tipoPregunta": "Multiple"
}

c. Realiza el mismo tipo de adaptación que apartado a pero con la enumeración Dificultad.

d. Implementa un adaptador de tipo personalizado, CategoriaAdapter, para la clase Categoria que convierta la categoría a una cadena:

{
  "categoria": "General"
}

e. Implementa un adaptador de tipo personalizado, PreguntaAdapter, para la clase Pregunta que convierta con el siguiente formato:

{
  "type": "multiple",
  "difficulty": "easy",
  "category": "Programación",
  "question": "¿Cuál de los siguientes lenguajes de programación es orientado a objetos puro?",
  "options": [
    {
      "enunciado": "Java",
      "correcta": true
    },
    {
      "enunciado": "Modula-2",
      "correcta": false
    },
    {
      "enunciado": "Python",
      "correcta": false
    },
    {
      "enunciado": "C",
      "correcta": false
    }
  ]
}

Ayuda: el adaptador de tipo personalizado JsonSerializer para la clase Pregunta debe tener en cuenta que el tipo de pregunta es multiple o boolean y debe instanciar la clase correspondiente. Además, el método serialize debe devolver un objeto JSON con el formato indicado (JsonObjetc objeto = new JsonObject()).

f. Implementa un adaptador de tipo personalizado, PreguntaMultipleAdapter, para la clase PreguntaMultiple que convierta con el siguiente formato:

{
  "type": "multiple",
  "difficulty": "easy",
  "category": "Programación",
  "question": "¿Cuál de los siguientes lenguajes de programación es orientado a objetos puro?",
  "correct_answer": "Java",
  "incorrect_answers": [
    "Modula-2", "Python", "C"
  ]
}
Última actualización: 23.09.2025

Ejercicios y recursos


Ejercicios con ficheros

Soluciones

Soluciones a ejercicios

Apuntes y recursos

Exámenes

UD 2. Acceso a BD remotas relacionales

UD 2. Acceso a BD remotas relacionales. Creación de una interfaz web sencilla (Vaadin)

Subsecciones de UD 2. Acceso a BD remotas relacionales

02.02 JDBC. Introducción a las Bases de datos relacionales.

En este apartado, a modo de resumen, veremos una introducción a bases de datos relacionales.

Subsecciones de 02.02 JDBC. Introducción a las Bases de datos relacionales.

02.01. Sistemas gestores de bases de datos


1. SQLite

Para facilitar el trabajo de aplicaciones sencillas, existen muchos SGBD relacionales orientados a archivo (embebidos) opensource como H2, SQLite, HSQL, tinySQL, smallSQL o comerciales:

Uno de los SGBD más empleados, sobre todo en dispositivos móviles, es SQLite.

Como trabajaremos con dependencias a los Drivers JDBC, cuyo archivo jar precisamos en nuestro proyecto y en el classpath de ejecución/compilación, recomendaría realizar un proyecto Maven, aunque podría descargarse el driver JDBC de SQLite y añadirse como biblioteca al proyecto Java.

Una de las mejores páginas para consultar información sobre SGBD es:

1.1. Dependencias Maven

Para trabajar con SQLite se precisa tener añadida la dependencia con los Driver JDBC de SQLite, por ejemplo, en Netbeans:

Dependencia JDBC SQlite en Netbeans Dependencia JDBC SQlite en Netbeans

Dependencia JDBC SQLite en IntelliJ IDEA Dependencia JDBC SQLite en IntelliJ IDEA

Puede hacerse a mano en el propio archivo pom.xml:

<dependencies>
  <dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.47.0.0</version>
  </dependency>
</dependencies>

A día de hoy la última versión es la 3.43.2.2

Los drives JDBC del SQLite pueden descargarse de:

Puedes ver un ejemplo de uso en la página oficial.

Se puede trabajar tanto con datos en memoria (durante la ejecución del programa) como en archivo:

1.2. Bases de datos en memoria

Bases de datos en memoria:

Connection conex = DriverManager.getConnection("jdbc:sqlite::memory:");

1.3. Bases de datos en archivo

En archivo:

Connection conex = DriverManager.getConnection("jdbc:sqlite:rutaArchivo.sqlite3");

El hecho de realizarlo en SQLite da portabilidad al proyecto, pues este SGBDR es orientado a archivo y no precisa estar instalado como servicio en ningún computador.

Como dice en la página del proyecto:

“SQLite es una biblioteca ’en proceso’ que implanta un motor de bases de datos SQL autónomo, sin servidor, sin configuración y transaccional…”

“SQLite es el motor de base de datos más utilizado del mundo. SQLite está integrado en todos los teléfonos móviles y la mayoría de las computadoras y viene incluido dentro de innumerables otras aplicaciones que la gente usa todos los días.”

1.4. Ayuda y referencias

2. DAO (Data Access Object)

3. Creación de una base de datos en H2

Para la creación de la BD podemos emplear los propios IDE, HeidiSQL, DBeaver (recomendación personal en este caso) o similar. En este caso tan concreto, recomendaría usar DBeaver, pues permite gestionar muchas bases de datos (prácticamente todas las que disponen de Drivers JDBC, pues este programa está escrito en Java):

Descarga para Windows:

Descarga de DBeaver Descarga de DBeaver

Es importante crear la base de datos y usar la biblioteca de la aplicación con el mismo formato 1.X o 2.X (no son compatibles):

Nueva base de datos Nueva base de datos

Creamos una nueva conexión:

Creación de la conexión a la base de datos Creación de la conexión a la base de datos

Seleccionamos el base de datos H2 1.X o la versión 2.X, en cuyo caso debemos añadir las dependencias del proyecto con la versión correspondiente:

Selección versión H2 Selección versión H2

Configuración:

Configuración de H2 Configuración de H2

2.1. Dependencias

Para la versión 2:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

Para la versión 1:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

3. Ejemplo

Por ejemplo, puedes crear la base de datos con de acuerdo con el script SQL o por medio de la interface gráfica (no entraré en detalles):

Ejemplo de relaciones en BD Ejemplo de relaciones en BD

CREATE TABLE PUBLIC.Debuxo (
    idDebuxo INTEGER NOT NULL AUTO_INCREMENT,
    nome CHARACTER VARYING(64) NOT NULL,
    CONSTRAINT DEBUXO_PK PRIMARY KEY (idDebuxo)
);
CREATE INDEX DEBUXO_NOME_IDX ON PUBLIC.DEBUXO (nome);
COMMENT ON TABLE PUBLIC.DEBUXO IS 'Debuxo da base de datos composto por figuras.';
COMMENT ON COLUMN PUBLIC.DEBUXO. idDebuxo IS 'Clave primaria';
COMMENT ON COLUMN PUBLIC.DEBUXO.nome IS 'Nome do debuxo';

CREATE TABLE PUBLIC.Shape (
    idDebuxo INTEGER NOT NULL,
    shape BINARY LARGE OBJECT,
    CONSTRAINT SHAPE_FK FOREIGN KEY (idDebuxo) REFERENCES PUBLIC.Debuxo(idDebuxo) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX SHAPE_IDDEBUXO_IDX ON PUBLIC.SHAPE (idDebuxo);
COMMENT ON TABLE PUBLIC.shape IS 'Figuras de dibujo';
COMMENT ON COLUMN PUBLIC.Shape. idDebuxo IS 'Referencia ó debuxo';
COMMENT ON COLUMN PUBLIC.Shape.shape IS 'BLOB con el objeto de la figura';

En el ejemplo de BD se emplea un tipo dato BLOB (binario grande), para guardar un objeto binario en la base de datos.

La configuración de la URL a la base de datos es la que aparece en las propiedades de la conexión:

"jdbc:h2:RutaABaseDatos\debuxos"

URL a base de datos H2 URL a base de datos H2

Los parámetros de la conexión deben ser:

JDBC_DRIVER = "org.h2.Driver"; // No se precisa en JDBC versión mayor a 4.0
DB_URL = "jdbc:h2:RutaABaseDatos\nomeBD”;

Ahora podemos proceder como cualquier otro proyecto de conexión a base de datos empleando los dos parámetros (el primero no se precisa desde JDBC 4.0).

Última actualización: 23.09.2025

02. Introducción a las bases de datos relacionales


1. Introducción a Bases de Datos Relacionales y SQL

Datos son información. Un dato es un hecho, como tu primer nombre. Una base de datos es una colección organizada de datos. En el mundo real, un archivador es un tipo de base de datos. Tiene carpetas, cada una con documentos. Las carpetas se organizan de alguna manera, a menudo alfabéticamente. Cada documento es como un dato. De manera similar, las carpetas en tu computadora son como una base de datos. Las carpetas proporcionan organización y cada archivo es un dato.

Una base de datos relacional está organizada en tablas, que constan de filas y columnas.

Hay dos formas principales de acceder a una base de datos relacional desde Java.

  1. Java Database Connectivity Language (JDBC): Accede a los datos como filas y columnas. JDBC es la API cubierta en este capítulo.

  2. Java Persistence API (JPA): Accede a los datos a través de objetos Java mediante un concepto llamado object-relational mapping (ORM). La idea es que no tienes que escribir tanto código y obtienes tus datos en objetos Java. LO veremos en la unidad de Hibernate, un framework para trabajar con JPA.

Una base de datos relacional se accede mediante Structured Query Language (SQL), un lenguaje de programación utilizado para interactuar con registros de bases de datos. JDBC funciona enviando un comando SQL a la base de datos y luego procesando la respuesta.

Además de las bases de datos relacionales, existe otro tipo de base de datos llamada base de datos NoSQL. Esto es para bases de datos que almacenan sus datos en un formato diferente a las tablas, como almacenes de claves/valores, almacenes de documentos y bases de datos basadas en gráficos. NoSQL lo veremos en unidades siguientes.

En los siguientes apartados, veremos una pequeña base de datos relacional que usaremos en algún ejemplo y veremos las sentencias SQL para acceder a ella.

1.1. Derby

En esta introducción veremos Derby (http://db.apache.org/derby) pero en el aula, para facilitar el trabajo emplearemos SQLite o H2, también haremos ejemplos con MadiaDB.

Derby es una base de datos pequeña en memoria. De hecho, sólo se necesita un archivo JAR para ejecutarlo.
La descarga e implantación es muy sencilla, pero os mostraré cómo hacerlo (podré un enlace al final)

También hay bases de datos “independientes”, orientadas a servicio, que podéis probar par instanalr un motor completo de base de datos. Los más populares (y recomendables) son: MySQL (www.mysql.com), MariaDB (https://mariadb.org/) o PostgreSQL (https://www.postgresql.org/),de código abierto y con más de 20 años de existencia.
Aunque las principales bases de datos tienen muchas similitudes, también tienen diferencias importantes y características avanzadas. Elegir la base de datos correcta para tu trabajo es una decisión importante que debes investigar mucho. Cualquier Sistema Gestor de Bases de Datos es bueno para practicar.

Diferencias entre MariaDB y MySQL

Hay muchos manuales para instalar y comenzar con cualquiera de estos Sistemas Gestores de Bases de Datos (SGBD o BDMS). Está fuera de los contenidos de esta materia configurar una base de datos, pero es algo que se precisa conocer si deseamos implantar nuestras aplicaciones de acceso a datos.

2. Ejemplo de una base de datos relacional

A modo de ejemplo, emplearemos una base de datos con dos tablas. Una tiene una fila por cada especie del zoológico. La otra tiene una fila por cada animal. Estas dos están relacionadas porque un animal pertenece a una especie. Estas relaciones son por las que este tipo de base de datos se llama base de datos relacional.

Tablas en nuestra base de datos relacional:

Especie:

idEspecie (PK) nome varchar(255) area (decimal)
1 Elefante Africano 9.5
2 Cebra 3.1

Animal:

idAnimal (PK) idEspecie (FK) nome, varchar (255) dataNacemento (timestamp)
1 1 Pepa 2001-05-06 02:15:00
2 2 Lola 2012-08-15 09:12:00
3 1 Dumbo 2022-09-09 10:36:00
4 1 Babar 2010-06-08 01:24:00
5 2 Rayas 2013-06-08 01:24:00

Hemos declarado dos tablas (después lo ajustaremos un poco), una se llama Especie y la otra Animal. Cada tabla tiene una clave primaria (PK), que nos da una forma única de referenciar cada fila. Después de todo, dos animales pueden tener el mismo nombre, pero no pueden tener la misma ID.

Nota: En el ejemplo, la clave primaria es sólo una columna. En algunas situaciones, es una combinación de columnas llamada clave compuesta. Por ejemplo, un identificador de estudiante y año podrían ser una clave compuesta. Hay dos filas y tres columnas en la tabla Especie y cinco filas y tres columnas en la tabla Animal.

2.1 Código para configurar la base de datos

En el código SQL se utilizan partes de SQL llamadas lenguaje de definición de base de datos (DDL) y lenguaje de manipulación de datos (DML).

Antes de ejecutar el código, debes agregar un archivo .jar al classpath o añadir la dependencia Maven. Agrega <PATH TO DERBY>/derby.jar al classpath. Asegúrate de reemplazar <PATH TO DERBY> con la ruta real en tu sistema de archivos.

Código de creación:

import java.sql.*;
 
public class SetupDerbyDatabase {
 
    public static void main(String[] args) throws Exception {
        
        String url = "jdbc:derby:zoo;create=true";

        try (Connection conexion = DriverManager.getConnection(url)) {
 
            // Para eliminar las tablas:
            // executar(conexion,"DROP TABLE Animal");
            // executar(conexion,"DROP TABLE Especie");
 
            executar(conexion,"CREATE TABLE Especie ("
                    + "idEspecie INTEGER PRIMARY KEY, "
                    + "nome VARCHAR(255), "
                    + "area DECIMAL(4,1))");
 
            executar(conexion,"CREATE TABLE Animal ("
                    + "idAnimal INTEGER PRIMARY KEY, "
                    + "idEspecie integer REFERENCES Especie (idEspecie), "
                    + "nome VARCHAR(255),"
                    + "dataNacemento TIMESTAMP)");
 
            executar(conexion,"INSERT INTO Especie VALUES (1, 'Elefante africano', 9.5)");
            executar(conexion,"INSERT INTO Especie VALUES (2, 'Cebra', 3.1)");
 
            executar(conexion,"INSERT INTO Animal VALUES (1, 1, 'Pepa', '1972−05−06 02:15')");
            executar(conexion,"INSERT INTO Animal VALUES (2, 2, 'Lola', '2009−08−15 09:12')");
            executar(conexion,"INSERT INTO Animal VALUES (3, 1, 'Dumbo', '2012−09−09 10:36')");
            executar(conexion,"INSERT INTO Animal VALUES (4, 1, 'Babar', '2010−06−08 01:24')");
            executar(conexion,"INSERT INTO Animal VALUES (5, 2, 'Rayas', '2015−11−12 03:44')");
 
            printCount(conexion,"SELECT count(*) FROM Animal");
        }
    }
 
    private static void executar(Connection conexion, String sql) throws SQLException {
        try (PreparedStatement ps = conexion.prepareStatement(sql)) {
            ps.executeUpdate();
        }
    }
 
    private static void printCount(Connection conexion, String sql)
                    throws SQLException {
        try (PreparedStatement ps = conexion.prepareStatement(sql)) {
            ResultSet rs = ps.executeQuery();
            rs.next();
            System.out.println(rs.getInt(1));
        }
    }
}

Se ejecuta desde línea de órdenes con la siguiente orden:

java -cp "<path_to_derby>/derby.jar" SetupDerbyDatabase.java

Lo más correcto es introducir la biblioteca de Derby en el directorio db/lib de Java y tener configurado de manera adecuada la variable JAVA_HOME: <JAVA_HOME>/db/lib/derby.jar

java −cp "/mi/home/jdk/db/lib/derby.jar:." SetupDerbyDatabase

En Windows:

java −cp "c:\program files\jdk\db\lib\derby.jar;." SetupDerbyDatabase

El programa se conecta a la base de datos y crea dos tablas. Luego carga datos en esas tablas.

De momento sólo es un ejemplo sencillo, veremos cómo una Connection y un PreparedStatement de varios modos.

3. Repaso de declaraciones SQL básicas

Hay cuatro tipos de operaciones para trabajar con los datos en las bases de datos. Se conocen como CRUD (Crear, Leer, Actualizar, Eliminar). Las palabras clave de SQL no coinciden con el acrónimo:

Tabla 21.1 Operaciones CRUD:

Operación Palabra Clave SQL Descripción
Crear INSERT Agrega una nueva fila a la tabla
Leer SELECT Recupera datos de la tabla
Actualizar UPDATE Cambia cero o más filas en la tabla
Eliminar DELETE Elimina cero o más filas de la tabla

Si ya conoces SQL, puedes saltar el resto de este apartado. Se muestra lo básico para aquellos que no saben o por si quieres repasar algún concepto básico.

A diferencia de Java, las palabras clave de SQL no distinguen entre mayúsculas y minúsculas. Esto significa que select, SELECT y Select son equivalentes. Muchas personas usan mayúsculas para las palabras clave de la base de datos para que destaquen. También es una práctica común usar snake case (guión bajo para separar “palabras”) en los nombres de las columnas. Tened en cuenta que en algunas bases de datos, los nombres de tabla y columna pueden ser sensibles a mayúsculas y minúsculas.

Al igual que los tipos primitivos de Java, SQL tiene varios tipos de datos. La mayoría son autoexplicativos, como INTEGER. También hay DECIMAL, que funciona de manera similar a un double en Java. El más extraño es VARCHAR, que significa “variable character” y es similar a un String en Java. La parte variable significa que la base de datos debe usar sólo el espacio necesario para almacenar el valor.

La declaración INSERT se utiliza generalmente para crear una nueva fila en una tabla; aquí tienes un ejemplo:

INSERT INTO Especie
VALUES (3, 'Elefante Africano', 10.8);

La declaración INSERT enumera los valores que queremos insertar. De forma predeterminada, utiliza el mismo orden en el que se definieron las columnas. Los datos de cadena están encerrados entre comillas simples.

La declaración SELECT lee datos de la tabla.

SELECT *
FROM Especie
WHERE idEspecie = 3;

La cláusula WHERE es opcional. Si la omites, se devuelven los contenidos de toda la tabla. El * indica que se devuelvan todas las columnas en el orden en que se definieron. Alternativamente, puedes enumerar las columnas que deseas que se devuelvan.

SELECT nome, area
FROM Especie
WHERE idEspecie = 3;

Es preferible enumerar los nombres de las columnas para mayor claridad. También ayuda en caso de que la tabla cambie en la base de datos.

También puedes obtener información sobre todo el resultado sin devolver filas individuales mediante funciones SQL especiales.

SELECT COUNT(*), SUM(area)
FROM Especie;

Esta consulta nos dice cuántas especies tenemos y cuánto espacio necesitamos para ellas. Devuelve sólo una fila ya que está combinando información (funciones de agregación), Si no hay filas en la tabla, la consulta devuelve una fila que contiene cero como respuesta.

La declaración UPDATE cambia los valores cero o más filas en la base de datos.

UPDATE Especie
SET area = area + .5
WHERE nome = 'Elefante Asiático';

Nuevamente, la cláusula WHERE es opcional. Si se omite, se actualizarán todas las filas de la tabla. La declaración UPDATE siempre especifica la tabla a actualizar y la columna a actualizar.

La declaración DELETE elimina una o más filas de la base de datos.

DELETE FROM Especie
WHERE nome = 'Elefante Asiático';

Y una vez más, la cláusula WHERE es opcional. Si se omite, se vaciará toda la tabla. ¡Así que ten cuidado! ;-)

Todo el SQL mostrado en esta sección es común en casi todos los SGBDR, mas SQL más avanzado, hay variación entre bases de datos, con funciones o tipos de datos diferentes.

Última actualización: 23.09.2025

03. PostgreSQL

Instalación de PostgreSQL sin privilegios de administrador

Muchas veces (instituto u otro centro educativo, organización,…) es muy poco probable que se tengan privilegios de administrador para instalar cualquier software ajeno, por lo que veremos cómo instalar PostgreSQL sin privilegios de administrador en Windows y Linux.

1. Instalación en Windows

Sigue los siguientes pasos para instalar PostgreSQL:

  1. Para configurar PostgreSQL, necesitamos descargar los binarios de PostgreSQL: https://www.enterprisedb.com/download-postgresql-binaries. Elige el archivo binario de la última versión de PostgreSQL.

  2. Crea una nueva carpeta en un directorio con control total y extrae estos archivos zip binarios en ella. (Preferiblemente, puedes extraer estos archivos binarios en ubicaciones como la unidad D:\ o E:). La estructura de la carpeta será algo similar a la siguiente:

Estructura de la carpeta de PostgreSQL Estructura de la carpeta de PostgreSQL

  1. Agrega esta ubicación del directorio “bin” en la variable PATH en las Variables de entorno de esta cuenta (de usuario).

Variables de entorno de usuario Variables de entorno de usuario

Ya está instalado en el Sistema Operativo. Ahora, necesitamos configurar la base de datos.

1.1. Verificación de la instalación

Para verificar si está instalado correctamente, usa los siguientes comandos.

El siguiente comando verifica la versión del servidor PostgreSQL:

postgres -V

Obtenemos una salida similar a la siguiente:

postgres (PostgreSQL) 17.4

El siguiente comando verifica la versión del cliente PostgreSQL:

psql -V

Obtenemos una salida similar a la siguiente:

psql (PostgreSQL) 17.4

1.2. Creación de una base de datos y asociar un usuario en PostgreSQL

Para crear una base de datos y asociar un usuario a ésta, la base de datos se inicializará en la ubicación que hemos especificado (carpeta de data en este caso, opción recomendable). La orden es la siguiente:

initdb -D "E:\99 - Portables\pgsql\data" -U postgres -E utf8

Debes sustituir la ruta por la que hayas elegido para la carpeta de PostgreSQL.

initdb -D path/to/db/server/ -U NAME -E utf8

Alguna de las opciones que podemos usar son:

  • -D path/to/db/server/: informa a initdb para inicializar la base de datos( cluster de base de datos) en una ubicación concreta especificada por el usuario. Después de especificar la ubicación, se creará implícitamente un nuevo directorio y se guardarán aquí todos los archivos de PostgreSQL y sus datos relacionados.
  • -U NAME o –username=NAME: crea un usuario con el nombre especificado y con todos los privilegios de superusuario.
  • -W: se utiliza para solicitar explícitamente una contraseña para el nuevo superusuario.
  • -E: indica la codificación que se utilizará para la base de datos.
  • -A METODO o –auth=MÉTODO: se usa para especificar el cifrado de la contraseña para conexiones locales.
  • –auth-local=METODO se usa para especificar el cifrado de la contraseña para conexiones locales por socket.
  • –auth-host=METODO se usa para especificar el cifrado de la contraseña para conexiones de red.
  • –locale=LOCALE: se usa para especificar la configuración regional.

La salida será similar a la siguiente:

E:\99 - Portables\pgsql>initdb -D .\data -U postgres -E utf8
The files belonging to this database system will be owned by user "pepecalo".
This user must also own the server process.

The database cluster will be initialized with locale "Galician_Spain.1252".
initdb: could not find suitable text search configuration for locale "Galician_Spain.1252"
The default text search configuration will be set to "simple".

Data page checksums are disabled.

creating directory data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... windows
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Europe/Paris
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    pg_ctl -D ^"^.^\data^" -l logfile start

1.3. Iniciar el sistema gestor de base de datos

  1. Inicia el sistema gestor de base de datos ejecutando:
pg_ctl -D "E:\99 - Portables\pgsql\data" -l logfile start

La salida será similar a la siguiente:

E:\99 - Portables\pgsql>pg_ctl -D .\data -l logfile start
waiting for server to start.... done
server started

El puerto por defecto de PostgreSQL es el 5432. Si deseas cambiar el puerto, puedes hacerlo en el archivo postgresql.conf en la carpeta data que hemos creado.

1.4. pgAdmin4

Aunque trabajaremos con Dbeaver, una vez que el servidor PostgreSQL esté en funcionamiento, puedes usar pgAdmin4 para administrar la base de datos. Para abrir pgAdmin4, sigue los siguientes pasos:

  1. Descarga pgAdmin4: https://www.pgadmin.org/download/.

Esta es una herramienta de administración de bases de datos para PostgreSQL y derivados, que puede ser instalada para un usuario concreto.

  1. Instala pgAdmin4 en la carpeta de tu elección. Por ejemplo, en la carpeta E:\99 - Portables\pgsql\pgAdmin4.

  2. Haz doble clic en la aplicación pgAdmin4 (ruta E:\99 - Portables\pgAdmin 4\runtime) para iniciar el programa (antes era versión Web pero ahora es versión de escritorio).

  3. Ahora haz clic en Servers en el lado derecho para crear un nuevo servidor para tu base de datos. Completa los detalles requeridos. Crea una conexión con el servidor local en el puerto 5432 y con el usuario postgres:

Conexión con el servidor local Conexión con el servidor local

  1. Haz clic en la sección de Databases para crear una nueva base de datos para tu trabajo y comenzar a usarla.

  2. Para detener la base de datos, utiliza el mismo comando utilizado para iniciar la base de datos como se usó arrancarla y sustituye start por stop.

pg_ctl -D "E:\99 - Portables\pgsql\data" -l logfile stop

Para usar funciones adicionales de PGSQL como el vaciado (vacuuming), upgrade, restore, etc., es posible que necesites configurar las rutas binarias para ello. Para esto, ve a File -> Preferences. Ahora desplázate hacia abajo hasta Paths y haz clic en Binary Paths. Especifica las rutas como el directorio de la carpeta bin de instalación de PgSQL:

  • EDB Advanced Server Binary Path: E:\99 - Portables\pgsql\bin
  • PostgreSQL Binary Path: E:\99 - Portables\pgsql\bin

2. Instalación en Ubuntu sin sudo

Referencia https://gist.github.com/usuario/832aceac6998e2f894e5780229920cb5

En este caso seguiremos la opción de compilación desde el código fuente.

2.1. Compilación e Instalación de PostgreSQL

Asegúrate de tener instalado el siguiente software:

  • Un compilador de C/C++, como GCC.
  • Otro software de soporte, consulta aquí.
  1. Descarga el código fuente: https://www.postgresql.org/ftp/source/.

    Puedes usar wget https://www.postgresql.org/ftp/source/v16.2/postgresql-17.4.tar.gz.

    • Nota: La versión puede cambiar, por lo que asegúrate de descargar la última versión.
  2. Descomprime el código fuente con tar xvzf postgresql-17.4.tar.gz.

tar -xzvf postgresql-[version].tar.gz
  1. Navega a la carpeta con cd postgresql-17.4/.

Nota: a veces es necesario tener instaladas las dependencias de compilación de PostgreSQL. Puedes instalarlas con sudo apt-get install build-essential ;-)

sudo apt-get update
sudo apt-get install build-essential libreadline-dev zlib1g-dev flex bison
  1. Actualiza la configuración del código fuente y compila:
cd postgresql-[version]
./configure --prefix=$HOME/postgresql
make
make install
Opcional. Número de núcleos
  1. Puedes compilar el código fuente con make world -j <num_cores_to_use>.

    • Verifica el número de núcleos de CPU en tu máquina con cat /proc/cpuinfo | grep processor | wc -l.
    • El argumento world significa que también se compilarán todos los módulos adicionales. Puedes ignorarlo.
    • Este paso puede tardar un tiempo si el computador no es lo suficientemente potente.
  2. Compila PostgreSQL con make install-world.

2.2. Configuración de PostgreSQL

  1. Añade los binarios de PostgreSQL a la ruta con:
echo 'export PATH="$HOME/postgresql/bin:$PATH"' >> ~/.bash_profile && source ~/.bash_profile`.
  1. Inicializa la base de datos con initdb -D $HOME/postgresql/data.

  2. Inicia la base de datos con pg_ctl -D $HOME/postgresql/data -l $HOME/postgresql/server.log start.

Ahora deberías tener PostgreSQL instalado y ejecutándose en tu sistema Ubuntu sin necesidad de sudo. Puedes acceder a la línea de comandos de PostgreSQL ejecutando psql y conectar a tu servidor PostgreSQL utilizando el usuario por defecto postgres. Por ejemplo:

psql -U postgres

Luego podrás comenzar a trabajar con tu base de datos PostgreSQL.

  1. Puedes cambiar la contraseña con ALTER USER <username> PASSWORD 'new_password_here';.

  2. Puedes eliminar la base de datos predeterminada creada con DROP DATABASE postgres;.

  3. Puedes crear una base de datos con el mismo nombre que la cuenta del sistema operativo con CREATE DATABASE <username>;.

Puedes conectarte a PostgreSQL simplemente usando psql.

3. Instalación en Ubuntu con Docker (sin sudo)

Si no puedes instalar PostgreSQL en tu sistema, puedes usar Docker para ejecutar PostgreSQL en un contenedor.

  1. (Ya instalado en el centro) Instala Docker siguiendo las instrucciones de la documentación oficial.
  2. Ejecuta el siguiente comando para descargar la imagen de PostgreSQL:
docker pull postgres
  1. Ejecuta el siguiente comando para ejecutar PostgreSQL en un contenedor:
docker run --name nombreContendor -e POSTGRES_PASSWORD=micontraseña -p 5432:5432 -d postgres
  1. Conéctate a PostgreSQL ejecutando el siguiente comando:
docker exec -it nombreContendor psql -U postgres
  1. Ahora puedes trabajar con PostgreSQL en tu sistema Ubuntu sin necesidad de instalarlo.

4. Instalación en Windows con Docker

Si no puedes instalar PostgreSQL en tu sistema, puedes usar Docker para ejecutar PostgreSQL en un contenedor.

  1. (Ya instalado en el centro) Instala Docker siguiendo las instrucciones de la documentación oficial.
  2. Ejecuta el siguiente comando para descargar la imagen de PostgreSQL:
docker pull postgres

o emplea la interfaz gráfica de Docker: Docker Desktop:

Descarga de la imagen de PostgreSQL Descarga de la imagen de PostgreSQL

Pulsando en Pull descargamos la imagen de PostgreSQL.

Imagen de Docker Hub

  1. Ejecuta el siguiente comando para ejecutar PostgreSQL en un contenedor:
docker run --name nombreContenedor -e POSTGRES_PASSWORD=contraseña -p 5432:5432 -d postgres
  • La opción -p 5432:5432 mapea el puerto 5432 del contenedor al puerto 5432 del host.
  • La opción -e POSTGRES_PASSWORD=contraseña establece la contraseña de la base de datos.
  • La opción --name nombreContenedor establece el nombre del contenedor.
  • La opción -d ejecuta el contenedor en segundo plano con el modo demonio.

Postgres estará ejecutándose en el contenedor en el puerto 5432.

Si lo haces desde Docker Desktop, puedes hacer clic en Run para ejecutar el contenedor y verlo en la pestaña Containers/Apps. Debes poner la variable de entorno POSTGRES_PASSWORD con la contraseña que desees.

  1. Conéctate a PostgreSQL ejecutando el siguiente comando:
docker exec -it nombreContenedor psql -U postgres

También puedes usar pgAdmin4 para administrar la base de datos. Para ello, sigue los pasos anteriores para instalar pgAdmin4 en Windows o el Dbveaver.

  1. Ahora puedes trabajar con PostgreSQL en tu sistema Windows sin necesidad de instalarlo.
Última actualización: 23.09.2025

04. PostgreSQL. Características y Operadores.

PostgreSQL – Características y Operadores

PostgreSQL es un sistema de gestión de bases de datos objeto-relacional potente y de código abierto que tiene como objetivo ayudar a los desarrolladores a construir aplicaciones y a los administradores a proteger la integridad de los datos y construir entornos tolerantes a fallos. Admite tipos de datos avanzados y características de optimización de rendimiento, como Ms-SQL Server y Oracle.

1. Características de PostgreSQL

  • Sistema de gestión de bases de datos de código abierto
  • Admite propiedades ACID
  • Técnicas de indexación diversas
  • Replicación basada en registros y basada en disparadores SSL
  • Soporte para JSON
  • Admite objetos geográficos
  • Compatible con orientado a objetos y ANSI-SQL 2008

2. Tipos de Datos en PostgreSQL

Numeric Character Date/Time Monetary Binary
Boolean Geométrico JSON Enumerado Búsqueda de texto
UUID Tipos de dirección de red Compuesto Identificadores de objeto Pseudo
BitString XML Rango Arrays pg_lsn
  • Datos Numéricos: smallint, integer, bigint, decimal, numeric, real, serial.
  • Datos de Carácter: varchar(n), text, char(n).
  • Datos de Fecha/Hora: timestamp, date, time, interval.
  • Tipo de Datos Monetarios: money.
  • Tipo de Datos Binarios: bytea (admite formato hexadecimal y de escape).
  • Tipo de Datos Booleano: boolean.
  • Tipos de Datos Geométricos: point, line, box, path, polygon, circle, lseg.
  • Tipos de Datos JSON: string, number, boolean, null.
  • Tipos de Datos Enumerados: enum.
  • Tipo de Datos UUID: uuid (almacena Identificadores Únicos Universales).
  • Tipos de Dirección de Red: cidr, inet, macaddr.
  • Pseudo Tipos: any, anyelement, anyarray, anyenum, anyrange, internal, record, trigger, event_trigger.
  • Tipos de BitString: bit(n), bit varying(n).
  • Tipos de Datos de Rango: int4range, int8range, numrange, tsrange (rango de marcas de tiempo), daterange.
  • Tipo de Datos pg_lsn: pg_lsn (almacena Número de Secuencia de Registro).

3. Operadores en PostgreSQL

Un operador manipula elementos de datos individuales y devuelve un resultado. Estas son las palabras reservadas utilizadas en la cláusula WHERE para realizar operaciones.

  • Operadores Aritméticos: +, -, *, /, %, ^, !
  • Operadores de Comparación: =, !=, <>, >, <, >=, <=
  • Operadores Lógicos: AND, NOT, OR
  • Operadores a Nivel de Bits: &, |

4. Instalación en Linux

a) Para instalar PostgreSQL, ejecuta el siguiente comando:

sudo apt install postgresql

O

sudo apt install postgresql postgresql-contrib

postgresql-contrib agregará algunas utilidades y funcionalidades adicionales.

b) Después de la instalación, cambia a la cuenta de Postgres:

sudo -i -u postgres

c) Ahora, puedes acceder al prompt de Postgres usando el comando psql.

5. Trabajando con Bases de Datos:

  • CREATE DATABASE: Se utiliza para crear la base de datos.

    CREATE DATABASE nombre_base_de_datos;
  • CREATE TABLE: Se utiliza para crear la tabla.

    CREATE TABLE nombre_tabla
    (columna_1 tipo_de_dato,
    columna_2 tipo_de_dato,
    ...
    columna_n tipo_de_dato);
  • INSERT: Se utiliza para insertar un nuevo registro (fila) en la tabla.

    INSERT INTO nombre_tabla (columna_1, columna_2 ,...)
    VALUES(valor_1, valor_2, ...);
  • SELECT: Se utiliza para obtener datos de una tabla de la base de datos.

    SELECT  
    columna_1, columna_2, .. columna_n
    FROM
    nombre_tabla;
  • La cláusula WHERE se utiliza para filtrar registros.

    SELECT  
    columna_1, columna_2, .. columna_n
    FROM
    nombre_tabla
    WHERE
    condición_1 AND condición_2;
  • LIMIT: Se utiliza para limitar el número de registros devueltos por una consulta.

    SELECT  
    columna_1, columna_2, .. columna_n
    FROM
    nombre_tabla
    LIMIT número_de_registros;
  • ORDER BY: Se utiliza para ordenar los registros devueltos por una consulta.

     SELECT
     columna_1, columna_2, .. columna_n
     FROM
     nombre_tabla
     ORDER BY columna_1 ASC|DESC, columna_2 ASC|DESC, ...;
  • OFFSET: Se utiliza para omitir un número específico de registros de una consulta.

    SELECT
    columna_1, columna_2, .. columna_n
    FROM
    nombre_tabla
    OFFSET número_de_registros;

6. Modificación de Tablas

Es posible modificar la estructura de una tabla existente utilizando la instrucción ALTER TABLE. PostgreSQL admite diversas acciones para realizar con ALTER TABLE, que se enumeran a continuación:

  • Agregar una columna a una tabla existente:

    ALTER TABLE nombre_tabla ADD COLUMN nuevo_nombre_columna TIPO;
  • Eliminar una columna de una tabla existente:

    ALTER TABLE nombre_tabla DROP COLUMN nombre_columna;
  • Renombrar una columna de una tabla existente:

    ALTER TABLE nombre_tabla RENAME COLUMN nombre_columna TO nuevo_nombre_columna;
  • Cambiar el nombre de una columna de una tabla existente:

    ALTER TABLE nombre_tabla ALTER COLUMN nombre_columna [SET DEFAULT valor | DROP DEFAULT];
  • Cambiar la restricción NOT NULL:

    ALTER TABLE nombre_tabla ALTER COLUMN nombre_columna [SET NOT NULL | DROP NOT NULL];
  • Agregar restricciones CHECK a una columna:

    ALTER TABLE nombre_tabla ADD CHECK expresion;
  • Agregar una restricción:

    ALTER TABLE nombre_tabla ADD CONSTRAINT nombre_restriccion definicion_restriccion;
  • Renombrar una tabla existente:

    ALTER TABLE nombre_tabla RENAME TO nuevo_nombre_tabla;
  • UPDATE: Se utiliza para actualizar o modificar datos existentes en la tabla.

    UPDATE nombre_tabla
    SET columna_1 = valor_1,
    columna_2 = valor_2, ...
    WHERE
    condición_1 AND condición_2;
  • DELETE: Se utiliza para eliminar fila(s) de la tabla.

    DELETE FROM nombre_tabla
    WHERE condición;

7. Restauración mediante la línea de comandos

Para restaurar una base de datos mediante la línea de comandos, seguiremos el siguiente procedimiento:

  1. En primer lugar, necesitamos iniciar sesión en el terminal de PostgreSQL a través de la línea de comandos. Para hacerlo, escriba el siguiente comando:
psql -U <nombre_usuario>
  1. Ahora podemos ver que hemos iniciado sesión correctamente en el terminal cliente psql y hemos obtenido el indicador de entrada de línea de comandos de PostgreSQL.

  2. Lo haremos esta vez a través del terminal de línea de comandos de PostgreSQL.

  3. Ahora crearemos una base de datos de marcador de posición para nuestro propósito que se utilizará para restaurar la copia de seguridad. Para hacerlo, ejecute el siguiente script.

CREATE DATABASE BackupDB ENCODING='UTF-8' OWNER='postgres';
  1. La base de datos ahora está creada. Ahora vamos a restaurarla. Para restaurar la base de datos, vamos a utilizar el comando pg_restore suministrado con algunos argumentos. Es importante tener en cuenta aquí que necesitamos salir del terminal psql para poder ejecutar el comando pg_restore. Para salir del terminal psql, escriba “\q”.

  2. Introduce el comando pg_restore con los siguientes argumentos:

pg_restore -U postgres -d backupdb -v "D:\Backup.sql"

La explicación detallada de los argumentos para PostgreSQL se puede encontrar en el sitio web oficial de PostgreSQL en la sección de documentación de pg_restore.

  1. Después de la restauración exitosa de la base de datos, veremos que nuestros esquemas se han restaurado junto con las tablas y sus datos.
Última actualización: 23.09.2025

02.03. Procesamiento de sentencias SQL.

En este apartado veremos las interfaces y clases que declara el API JDBC.

Subsecciones de 02.03. Procesamiento de sentencias SQL.

01. Procesamiento de sentencias SQL.


Introducción

  • La API Java JDBC (Java Database Connectivity) permite que las aplicaciones Java se conecten a SGBD relacionales.

  • La API JDBC permite consultar y actualizar, así como procedimientos almacenados u obtener metadatos sobre la base de datos relacionales (como MySQL, PostgreSQL, MS SQL Server, Oracle, H2 Database, etc.)

  • La API Java JDBC forma parte del SDK, por lo que está disponible para todas las aplicaciones Java.

Interface JDBC Interface JDBC

  • Java proporciona conexión a bases de datos mediante JDBC (Java Database Connection) que proporciona una interface a muchos tipos de bases de datos.

  • Oculta las diferencias debajo de SQL y proporciona un conjunto de interfaces que son una abstracción de la funcionalidad de la base de datos.

  • Nos conectamos desde java con unos controladores (drivers), implementaciones de las interfaces JDBC del API que pueden haber sido escritos en puro Java, para ser 100% portables, o pueden implicar un componente nativo. Un ejemplo es el puente JDBC-ODBC, que depende del S.O. y sólo se puede ejecutar en Windows.

Características

  • JDBC es independiente del Sistema Gestor de BD.

  • JDBC no es independiente de SQL: el dialecto de SQL utilizado por diferentes bases de datos varía ligeramente dependiente del SGBD (emplea SQL estándar)

  • NO es para SGBD no relacionales como MongoDB, Cassandra, Dynamo, etc, que tienen su propia biblioteca Java.

Elementos e interfaces relevantes:

  • Driver JDBC.
  • Connection.
  • Statement.
  • PreparedStatement.
  • CallableStatement.
  • ResultSet.
  • Batch Updates.
  • Transactions.
  • DatabaseMetadata.

Ejemplos de SGBD relacionales

SQLite: https://sqlite.org/index.html HSQLDB: https://hsqldb.org/ (HyperSQL database management system) H2Database MariaDB PostgreSQL Derby tinySQL: <http://priede.bf.lu.lv/ftp/pub/DatuBazes/tinySQL/tinySQL.htm SmallSQL: http://www.smallsql.de/ Microsoft SQL Server Oracle

Arquitectura JDBC Arquitectura JDBC

import java.sql.*;
public class EjemploJDBC {
   public static void main(String[] args) throws ClassNotFoundException {
      Class.forName("org.h2.Driver");
      String url = "jdbc:h2:~/prueba";   //URL específica de la base de datos
      String usuario = "sa";
      String password = "";
      try(Connection conexion = DriverManager.getConnection(url, usuario, password)) {
         try(Statement st = conexion.createStatement()){
             String sql = "select * from alumno";
             try(ResultSet result = st.executeQuery(sql)){ // Cierra automáticamente.
                 while(result.next()) {
                     String nome = result.getString(nome");
                     long idade  = result.getLong(“idade");
                 }
             }
         }
      } catch (SQLException e) { /* … */ }
    }
}

El API JDBC

Existe un módulo específico en el API Java SE para trabajar con bases de datos (creado en Java 9):

https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/module-summary.html

Gráfica del módulo java.sql Gráfica del módulo java.sql

El paquete principal es java.sql, pero desde el API JDBC 4.3 se incluye tanto el java.sql, denominado API principal de JDBC, como el javax.sql, denominado API del paquete opcional JDBC. Esta API JDBC completa está incluida en Java Standard Edition (Java SE), desde la versión 7.

  • java.sql: API principal para acceder y procesar datos almacenados en una fuente de datos (normalmente una base de datos relacional) utilizando el lenguaje de programación Java. Incluye un marco mediante el cual se pueden instalar diferentes controladores de forma dinámica para acceder a diferentes fuentes de datos. Aunque la API JDBC está diseñada principalmente para pasar declaraciones SQL a una base de datos, permite leer y escribir datos de cualquier fuente de datos con formato tabular. Existen unas interfaces de lectura/escritura en javax.sql.RowSet, que se puede personalizar para usar y actualizar datos de una hoja de cálculo, un archivo plano o cualquier otra fuente de datos tabulares. Incluye:
    • Clases e interfaces para establecer conexiones a BBD con la clase DriverManager: DriverManager, SQLPermission, Driver, DriverPropertyInfo.
    • Envío de sentencias a la base de datos: Statement, PreparedStatement, CallableStatement, Connection, Savepoint.
    • Obtención y actualización de resultado s de una consulta: ResultSet.
    • Mapeo de tipos SQL a clases e interfaces Java: Array, Blob, Clob, Date, NClob, Ref, RowId, Struct, SQLXML, Time, Timestamp, Types.
    • Mapeo personalizado de tipos SQL definidos por el usuario (UDT) a clases Java: SQLDat, SQLInput, SQLOuput.
    • Metadatos: DatabaseMetaData, ResultSetMetaData, ParameterMetaData.
    • Excepciones: SQLException, SQLWarning, DataTruncation, BatchUpdateException.
  • javax.sql: API para el acceso y procesamiento de fuentes de datos del lado del servidor desde el lenguaje de programación Java. Complementa el paquete java.sql y, a partir de la versión 1.4, se incluye en Java Platform, Standard Edition (Java SE). Sigue siendo una parte esencial de Java Platform, Enterprise Edition (Java EE).

Los paquetes relacionados:

  • API para transacciones distribuidas: javax.transaction.xa, define el contrato entre el gestor de transaccione y el gestor recursos, lo que permite al administrador de transacciones dar de alta y eliminar objetos de recursos (proporcionados por el controlador del administrador de recursos) en transacciones JTA.
  • java.util.logging: proporciona las clases e interfaces de las funciones principales de log de la plataforma Java 2. El objetivo principal del API de logging es respaldar el mantenimiento y servicio del software en los sitios de los clientes.
  • Módulo java.xml: declara y define el API de Java para procesamiento XML (JAXP = Java APIs for XML Processing).

Etapas de procesamiento de sentencias

En general, para procesar cualquier sentencia SQL con JDBC, sigue estos pasos:

  1. Establecer una conexión (Connection)
  2. Crea una declaración (Statement)
  3. Ejecuta la consulta (executeQuery/executeUpdate/execute)
  4. Procesa el objeto ResultSet, en el caso de ser de consulta)
  5. Cierra la conexión (de manera automática con try-catch-with-resources)

Por ejemplo, el método, Juego.showTabla el contenido de la tabla Juego:

public static void showTabla(Connection con) throws SQLException {
    String query = "select nombre, idDesarrollador, precio, ventas, total from Juego";
    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String nombreJuego = rs.getString("nombre");
            int idDesarrollador = rs.getInt("idDesarrollador");
            float precio = rs.getFloat("precio");
            int ventas = rs.getInt("ventas");
            int total = rs.getInt("total");
            System.out.println(nombreJuego + ", " + idDesarrollador + ", " + precio +
                            ", " + ventas + ", " + total);
        }
    } catch (SQLException e) {
        // Gestión de la excepción.
    }
}

1. Establecimiento Connection

Primero, establece una conexión con la fuente de datos que deseas utilizar. Una fuente de datos (Data source) puede ser un sistema de gestión de bases de datos (DBMS), un sistema de archivos heredado u otra fuente de datos con un controlador JDBC correspondiente. Esta conexión está representada por un objeto Connection.

2. Creación de Statement

Un Statement es una interfaz que representa una sentencia SQL. Si se invocan métodos de consulta (executeQuery) sobre un Statement y generan objetos ResultSet, una tabla de datos que representa un conjunto de resultados de base de datos.

Por ejemplo, Juego.showTabla crea un objeto Statement con el siguiente código:

stmt = con.createStatement();

Existen tres tipos diferentes de declaraciones:

  • Statement: Se utiliza para implementar sentencias SQL simples sin parámetros.
  • PreparedStatement: (hereda de Statement.) Se utiliza para compilar previamente (precompilar) sentencias SQL que pueden contener parámetros de entrada.
  • CallableStatement: (hereda de PreparedStatement.) se utiliza para ejecutar procedimientos almacenados que pueden contener parámetros de entrada y salida.

3. Ejecución de consultas: execute, executeQuery, executeUpdate

Para ejecutar una consulta, llama a un método de tipo execute de Statement. Existen 3 versiones:

  • execute: devuelve true si el primer objeto que devuelve la consulta es un objeto ResultSet. Se utiliza este método si la consulta podría devolver uno o más objetos ResultSet. Después se recuperan los objetos ResultSet devueltos por la consulta llamando repetidamente a Statement.getResultSet.
  • executeQuery: devuelve un objeto ResultSet.
  • executeUpdate: devuelve un entero que representa el número de filas afectadas por la sentencia SQL, con sentencias SQL INSERT, DELETE o UPDATE.

Por ejemplo, Juego.showTabla ejecutó un objeto Statement con el siguiente código:

ResultSet rs = stmt.executeQuery(query);

4. Obtención de objetos ResultSet

Para acceder a los datos de un objeto ResultSet se realiza a través un cursor, que no es un cursor de la base de datos. Es un puntero que apunta a una fila de datos en el objeto ResultSet. Inicialmente, el cursor se encuentra antes de la primera fila.

Existe varios métodos definidos en el objeto ResultSet para mover el cursor (next, previous,…)

Por ejemplo, Juego.showTabla llama repetidamente al método ResultSet.next para mover el cursor hacia adelante una fila. Cada vez que llama a next, el método obtiene los datos en la fila donde se encuentra actualmente el cursor:

ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
    String nombreJuego = rs.getString("nombre");
    int idDesarrollador = rs.getInt("idDesarrollador");
    float precio = rs.getFloat("precio");
    int ventas = rs.getInt("ventas");
    int total = rs.getInt("total");
    System.out.println(nombreJuego + ", " + idDesarrollador + ", " + precio +
                        ", " + ventas + ", " + total);
}
// ...

5. Cierre de Conexiones

Siempre que no precisemos más los objetos Connection, Statement o ResultSet, llama a su método close para liberar inmediatamente los recursos que está utilizando.

Es mejor recomendación emplear una declaración try-with-resources para cerrar automáticamente los objetos Connection, Statement y ResultSet, independientemente de si ha lanzado una SQLException. (JDBC lanza una SQLException cuando encuentra un error durante una interacción con una fuente de datos.)

Como sabes, una declaración automática de recursos consta de una declaración try y uno o más recursos declarados. Por ejemplo, el método Juego.showTabla cierra automáticamente su objeto Statement, de la siguiente manera:

public static void viewTable(Connection con) throws SQLException {
    String query = "select nombre, idDesarrollador, precio, ventas, total from Juego";
    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String nombreJuego = rs.getString("nombre");
            int idDesarrollador = rs.getInt("idDesarrollador");
            float precio = rs.getFloat("precio");
            int ventas = rs.getInt("ventas");
            int total = rs.getInt("total");
            System.out.println(nombreJuego + ", " + idDesarrollador + ", " + precio +
                            ", " + ventas + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

La siguiente declaración es una declaración try-with-resources, que declara un recurso, stmt, que se cerrará automáticamente cuando el bloque try finalice:

try (Statement stmt = con.createStatement()) {
    // ...
}

(El usuario de la base de datos es el usuario por defecto, “sa”, sin comillas, y la contraseña en blanco) Para permitir el uso de nombres en CamelCase en H2 JDBC Driver versión 2, es necesario agregar la propiedad DATABASE_TO_UPPER=FALSE en la URL de conexión.

Por ejemplo, si la URL de conexión es jdbc:h2:/test, la URL de conexión con la propiedad DATABASE_TO_UPPER=FALSE sería *jdbc:h2:/test;DATABASE_TO_UPPER=FALSE*. URL: jdbc:h2:ruta_a_baseDatos/JuegosH2

Ejercicio. Crear y transferir datos JSON-BD

Descarga de datos JSON y almacenamiento en una base de datos SQLite. Para ello debes ampliar el ejercicio anterior de JSON.

Al menos debes haber realizado los adaptadores de tipo, las clases del modelo para poder realizar de manera mejor diseñada el apartado i), que hace referencia a BD. Si no está hecho, debes leer el JSON y guardar los datos en la base de datos SQLite creada, con las tablas:

Plataforma: idPlataforma, nombre. Genero: idGenero, nombre. Juego: idJuego, titulo, miniatura (varchar), estado, descripción, url, idGenero (FK), idePlataforma (FK), editor, desarrollador, fecha, urlFreeToGame. Imagen: idImagen, idJuego (FK), url, imagen (tipo BLOB). De momento, sólo se guardará la URL a la imagen.

Referencias: https://www.freetogame.com/api-doc

Disponemos de un archivo JSON los dos datos de un juego:

  • Las plataformas pueden ser: pc, browser, all. (Incorpóralas a la tabla Plataforma)
  • Las categorías (géneros) pueden ser:
    • mmorpg, shooter, strategy, moba, racing, sports, social, sandbox, open-world, survival, pvp, pve, pixel, voxel, zombie, turn-based, first-person, third-Person, top-down, tank, space, sailing, side-scroller, superhero, permadeath, card, battle-royale, mmo, mmofps, mmotps, 3d, 2d, anime, fantasy, sci-fi, fighting, action-rpg, action, military, martial-arts, flight, low-spec, tower-defense, horror, mmorts. (Incorpóralas a la tabla Genero)
  • La ordenación puede ser: release-date, popularity, alphabetical o relevance
{
    "id": 452,
    "title": "Call Of Duty: Warzone",
    "thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
    "status": "Live",
    "short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
    "description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
    "game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
    "genre": "Shooter",
    "platform": "Windows",
    "publisher": "Activision",
    "developer": "Infinity Ward",
    "release_date": "2020-03-10",
    "freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
    "minimum_system_requirements": {
        "os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
        "processor": "Intel Core i3-4340 or AMD FX-6300",
        "memory": "8GB RAM",
        "graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
        "storage": "175GB HD space"
    },
    "screenshots": [
        {
            "id": 1124,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg"
        },
        {
            "id": 1125,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg"
        },
        {
            "id": 1126,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg"
        },
        {
            "id": 1127,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg"
        }
    ]
}

a) Crea las clases de la aplicación:

  • Image: con identificador, URL y ¡un array de bytes con la imagen!.
  • Plataforma: enumeración con 3 posibles valores, BROWSER, PC, ALL.
  • Game: con identificador, título, miniatura (tipo Image), descripción, url para jugar, género, plataforma (de tipo Plataforma), fecha de realización (LocalDate) y una lista de imágenes.
  • BrowserGame: hereda de Game y se trata de un juego para navegador, por lo que su categoría será BROWSER.

b) Haz una sencilla aplicación que, a partir de el JSON de un Game, cree un juego, pero sólo el identificador, el título, la descripción, la URL, … sin miniatura ni la lista de imágenes.

c) Haz que el juego se pueda guardar en un archivo de texto con el nombre: nombre del juego.txt y la versión toString del Game dentro de él. Emplea Java IO, no Files.

d) Como sólo nos interesan los juegos de navegador para jugar en clase mientras Pepe explica, haremos una aplicación que descargue la lista de juegos de:

https://www.freetogame.com/api/games?platform=browser

Empleando un InstanceCreator para que asigne la plataforma BROWSER al constructor de Game.

e) Amplía el ejercicio anterior para que también recupere las imágenes, sin los bytes, sólo la url. La miniatura tendrá siempre id igual a 0.

f) Amplía el ejercicio apartado anterior para que guarde recupere también la imagen y la almacene en el array de bytes. Ved nota [1]

g) Usando el API haz una aplicación que pida un identificador de objeto y lo descargue, tanto en un fichero de texto como las imágenes. Previamente debe “deserializar el objeto en el tipo Game”.

h) Si deseas hacer una aplicación gráfica, puedes ver la nota 2, en la que explico cómo crear un ImageIcon a partir de una array de bytes.

i) Haz diseña una base de datos SQLite con la estructura de los datos del JSON y crea un aplicación que descargue los juegos y los guarde en la base de datos.:

  • Crea la base de datos e introduce los datos “estáticos” de la Plataforma y el Género.
  • Realiza un programa que lea los archivos JSON, de la URL: https://www.freetogame.com/api/game?id=X, pasándole el id del Juego, desde 1 al número de juegos que consideres. Ten en cuenta que el juego podría no existir devolviendo:
{"status":0,"status_message":"No game found with that id"}
  • Lee los datos del JSON, por medio de un JsonReader (o un JsonParser) y guárdalos en la base de datos para cada juego, teniendo en cuenta que debes realizar los pasos que hemos comentado:
    • Establecer conexión con DriverManage.getConnection.
    • Crear sentencia, Statement.
    • Ejecutar sentencia de tipo executeUpdate(INSERT INTO …).

Para evitar problemas con los caracteres especiales, comillas, etc. usa como base el siguiente ejemplo:

// Se supone que ya hemos creado la conexión y creamos una sentencia preformateada.
// Las ? son los parámetros de la sentencia.
PreparedStatement ps= conexion.prepareStatement("UPDATE Filosofo set nome=? , apelidos=? where idFilosofo=?"); // Sólo se realiza al principio y luego se reutiliza para cada inserción o actualización. CON INSERT sería igual, cambiando la sentencia.

// asignamos los parámetros a la consulta. En nuestro caso serían los valores a insertar en las tablas.
ps.setString(1, "Ludwig Josef Johann");
ps.setString(2, "Wittgenstein");
ps.setLong (3, 1);
int filasAfectadas = ps.executeUpdate(); // típicamente devolverá 1 o 0.

// Dentro del bucle podemos volver a insertar nuevos valores sin tener que crear una nueva sentencia.
ps.setString(1, "Bertrand Arthur William");
ps.setString(2, "Russell");
ps.setLong (3, 1);
int filasAfectadas = ps.executeUpdate();

Nota 1. Guardar una imagen en un array de bytes:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ImageToBytes {
    public static void main(String[] args) {
        try {
            // 1. Crea un objeto fis de tipo InputStream a la imagen.
            // Ya deberías saberlo
            // 2. Crea un flujo de salida a un array de bytes:
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024]; // buffer
            for (int readNum; (readNum = fis.read(buf)) != -1;) {
            // Escribimos en el array de bytes
                bos.write(buf, 0, readNum); 
            }
            // Convertimos el flujo de bytes en un array
            byte[] bytes = bos.toByteArray();
            System.out.println("Imagen convertida a bytes: " + bytes);
        } catch (IOException e) {
            // …
        }
    }
}

Nota 2. ImageIcon a partir de un array de bytes:

import java.io.FileOutputStream;
import java.io.IOException;

public class BytesToImageFile {
    public static void main(String[] args) {
        try {
            byte[] bytes = new byte[] { 0x00, 0x01, 0x02, };
            // Ya sabes que hay mejores maneras de crear flujos, más 
            // eficientes, pero a modo de ejemplo.
            FileOutputStream fos = 
                new FileOutputStream("ruta/a/tu/imagen.jpg");
            fos.write(bytes);
            fos.close(); // mejor, try-catch-with-resources.
            System.out.println("Imagen guardada en disco.");
        } catch (IOException e) {
            // ..
        }
    }
}
Última actualización: 23.09.2025

02. Conexiones a BD


La interface Connection

Como se ha comentado anteriormente, la fuente de datos puede ser cualquier fuente que tenga un controlador JDBC: un sistema de gestión de bases de datos (DBMS), un sistema de archivos heredado u otra fuente de datos.

Existen dos clases principales para establecer la conexión:

  • DriverManager: clase totalmente implementada que conecta una aplicación a una fuente de datos, indicada por medio de una URL de base de datos.
    Cuando esta clase intenta establecer una conexión por primera vez, carga automáticamente cualquier controlador JDBC 4.0 (o superior) encontrado dentro del classpath. Se precisa cargar de manera manual cualquier controlador con JDBC inferior a 4.0

  • DataSource:: interfaz se prefiere sobre DriverManager porque permite que los detalles sobre la fuente de datos subyacente sean transparentes para tu aplicación. Las propiedades de un objeto DataSource se configuran para que represente una fuente de datos concreta. Es el modo de conexión preferido para aplicaciones Java EE.

DataSource

En principio emplearemos la clase DriverManager en lugar de la clase DataSource porque es más fácil de usar y, en general, las aplicaciones cliente multiplataforma no requieren las características avanzadas de la clase DataSource que inicialmente estaban diseñadas para ser empleadas en aplicaciones Java EE.

1. La clase DriverManager

Para establecer la conexión con la clase DriverManager sew invoca al método DriverManager.getConnection. El siguiente método de ejemplo establece una conexión de base de datos:

// Suponemos que este método está declarado dentro de una clase ConnectionManager que tiene
// - Un atributo sxbd de tipo String, para guardar el nombre del SGBD.
// - Un atributo con el nombre del servidor, nomeServidor
// - Un atributo con el número de puerto.
// - Un atributo para el usuario y otro para la contraseña.
public Connection getConnection() throws SQLException {

    Connection conn = null;
    // Propiedades de la conexión
    Properties propiedadesCon = new Properties();
    propiedadesCon.put("user", this.userName);
    propiedadesCon.put("password", this.password);

    if (this.sxbd.equals("mysql")) {
        conn = DriverManager.getConnection("jdbc:" + this.sxbd + "://" +
                   this.nomeServidor + ":" + this.puerto + "/", propiedadesCon);
    } else if (this.sxbd.equals("derby")) {
        conn = DriverManager.getConnection("jdbc:" + this.sxbd + ":" + this.dbName +
                   ";create=true", propiedadesCon);
    }
    System.out.println("Conexión establecida con la BD");
    return conn;
}

El método DriverManager.getConnection(...) establece una conexión de base de datos. Este método requiere una URL de base de datos, que varía según el SGBD/DBMS. El método devuelve un objeto Connection, que representa una conexión con el DBMS o una base de datos específica. Consulta la base de datos a través de este objeto.

Por ejemplo:

  • MySQL: jdbc:mysql://localhost:3306/, donde localhost es el nombre del servidor que aloja a la base de datos y 3306 es el número de puerto.

  • Java DB: jdbc:derby:testdb;create=true, donde testdb es el nombre de la base de datos a la que conectarse y create=true informa al SGBD a crear la base de datos.

    Nota:esta URL establece una conexión de base de datos con el controlador Java DB Embedded. Java DB también incluye un controlador Network Client, que utiliza una URL diferente.

El método anterior especifica el nombre de usuario y la contraseña necesarios para acceder al DBMS con un objeto Properties.

Cuando se usa un driver de un proveedor, la documentación informará del subprotocolo que usa, esto es, que pone después de “jdbc:subprotocolo:….” en la URL.

Formato URl a base de datos Formato URl a base de datos

Ejemplos:

SQLite: Driver: org.sqlite.JDBC URL: jdbc:sqlite:rutaArchivo

JDBC-ODBC: Driver: sun.jdbc.odbc.JdbcOdbcDriver URL: jdbc:odbc:dsn

MySQL: Driver: com.mysql.cj.jdbc.Driver URL: jdbc:mysql://localhost:3306/nomeBD

H2Database: Driver: org.h2.Driver URL: jdbc:h2:rutaArchivo

Oracle: Driver: oracle.jdbc.driver.OracleDriver URL: jdbc:oracle:thin@host:porto:bd

HSQLDB Driver: org.hsqldb.jdbc.JDBCDriver (hsqldb.jar) URL: jdbc:hsqldb:file:{folder} URL: jdbc:hsqldb:hsql://{HOST}[:{PORT}]

Nota. URL JDBC y Drivers
  • Generalmente, en la URL de la base de datos, también especifica el nombre de una base de datos existente a la que deseas conectarte.Por ejemplo: jdbc:mysql://localhost:3306/jugadores representa la URL de conexión a una base de datos MySQL llamada “jugadores”. En los caso de bases de datos en memoria no se especifica, porque debe ser creada previamente.
  • En versiones anteriores de JDBC a 4.0, para obtener una conexión, había que cargar previamente el Drive invocando al método estático Class.forName, que debía recoger un objeto de tipo java.sql.Driver.
  • Cada Driver JDBC contiene una o más clases que implementan la interfaz java.sql.Driver. Los controladores para Java DB son org.apache.derby.jdbc.EmbeddedDriver y org.apache.derby.jdbc.ClientDriver, y el de MySQL Connector/J es com.mysql.cj.jdbc.Driver. En los ejemplo anteriores hemos visto cómo se denominan las implementaciones para otros SGBD que emplearemos durante esta unidad y parte del curso.

Cualquier Driver JDBC 4.0 o superior que se encuentre en el classpath (o en las implementaciones o dependencias del proyecto Java) se carga automáticamente. (Sin embargo, debe cargarse de manera explícita cualquier Driver anterior a JDBC 4.0 con el método Class.forName).

2. Especificando URL de Conexión a la Base de Datos

Una URL de conexión de base de datos es una cadena que el controlador JDBC utiliza para conectarse a una base de datos. Puede contener información sobre dónde buscar la base de datos, el nombre de la base de datos a la que conectarse y propiedades de configuración.

La sintaxis exacta de una URL de conexión de base de datos depende del SGBD:

Formato URl a base de datos Formato URl a base de datos

Java DB Database Connection URLs: Derby

La siguiente es la sintaxis de la URL de conexión de base de datos para Java DB:

jdbc:derby:[subsubprotocol:][databaseName][;attribute=value]*

  • subsubprotocol especifica dónde Java DB debe buscar la base de datos, ya sea en un directorio, en memoria, en un classpath o en un archivo JAR. Típicamente, se omite.

  • databaseName es el nombre de la base de datos a la que conectarse.

  • attribute=value representa una lista opcional separada por punto y coma de atributos. Estos atributos permiten informar al SGBD de los parámetros de conexión, incluyendo la:

    • Creación de la base de datos indicada en la URL.
    • Encriptación de la base de datos.
    • Indicar directorios para almacenar información de log y traza.
    • Indicar el nombre de usuario y contraseña para conectarse a la base de datos.

Referencias:

Java DB es un distribución de la base de datos Open Source Apache Derby. Desde el 2015, JAvaDB no se incluye en JDK y fue eliminado de JDK 7 y 8 el 17 de julio del 2018. JavaDB ha sido redirigido a Apache Derby, para emplear JavaDB debe usarse la versión del Proyecto Apache Derby.

Apache Derby Apache Derby: Quick Start

MySQL Connector/J Database URL

La siguiente es la sintaxis de la URL de conexión de base de datos para MySQL Connector/J:

jdbc:mysql://[host][,failoverhost...][:port]/[database][?propertyName1][=propertyValue1][&propertyName2][=propertyValue2]...

  • host:port es el nombre de host y el número de puerto de la computadora que aloja la base de datos. Si no se especifica, los valores predeterminados de host y puerto son 127.0.0.1 y 3306, respectivamente.

  • database es el nombre de la base de datos a la que conectarse. Si no se especifica, se realiza una conexión sin una base de datos predeterminada.

  • failover es el nombre de una base de datos en espera (MySQL Connector/J admite failover).

  • propertyName=propertyValue representa una lista opcional separada por ampersand de propiedades. Estas propiedades te permiten informar a MySQL Connector/J la realización de varias tareas y configuraciones.

Ejemplo con MariaDB:

El conector más reciente de MariaDB es compatible con MySQL 5.5.3 o superior, además de con MariaDB.

Se ajustan a las especificaciones de JDBC 4.2.

Class.forName("org.mariadb.jdbc.Driver"); // Sigue funcionando pero no se precisa.
Connection connection = DriverManager.getConnection("jdbc:mariadb://localhost:3306/DB?user=root&password=myPassword");

Todos los parámetros de conexión pueden consultarse en:

https://mariadb.com/kb/en/about-mariadb-connector-j/

Referencias:

MariaDB Connector/J Descarga de conectores MariaDB MySQL Documentation Conector JDBC de MySQL

Otros sistemas gestores de bases de datos

Cuando se usa un driver de un proveedor, la documentación informará del subprotocolo que usa, esto es, que pone después de “jdbc:subprotocolo:….” en la URL.

SQLite: Driver: org.sqlite.JDBC URL: jdbc:sqlite:rutaArchivo

JDBC-ODBC: Driver: sun.jdbc.odbc.JdbcOdbcDriver URL: jdbc:odbc:dsn

MySQL: Driver: com.mysql.cj.jdbc.Driver URL: jdbc:mysql://localhost:3306/nomeBD

H2Database: Driver: org.h2.Driver URL: jdbc:h2:rutaArchivo

Oracle: Driver: oracle.jdbc.driver.OracleDriver URL: jdbc:oracle:thin@host:porto:bd

HSQLDB Driver: org.hsqldb.jdbc.JDBCDriver (hsqldb.jar) URL: jdbc:hsqldb:file:{folder} URL: jdbc:hsqldb:hsql://{HOST}[:{PORT}]

Última actualización: 23.09.2025

03. Excepciones SQLException


Gestión de Excepciones SQLException

Cuando JDBC encuentra un error durante una interacción con una base de datos a la que está conectado un objeto Connection, lanza una instancia de SQLException.La instancia de SQLException contiene la siguiente información que facilita encontrar la causa del error:

  • Una descripción del error. Recupera el objeto String que contiene esta descripción llamando al método SQLException.getMessage().

  • Un código SQLState: códigos y significados estandarizados por ISO/ANSI y Open Group (X/Open), aunque algunos códigos se han reservado para que los definan los proveedores de bases de datos. Este objeto String consta de cinco caracteres alfanuméricos. Recupera este código llamando al método SQLException.getSQLState().

  • Un código de error: valor entero que identifica el error que causó que la instancia de SQLException se lanzara. Su valor y significado son específicos de la implementación y podrían ser el código de error real devuelto por la fuente de datos. Recupera el error llamando al método SQLException.getErrorCode().

  • Una causa. Una instancia de SQLException podría tener una relación causal, que consiste en uno o más objetos Throwable que causaron que la instancia de SQLException se lanzara. Para navegar por esta cadena de causas, llama recursivamente al método SQLException.getCause() hasta que se devuelva un valor nulo.

  • Una referencia a excepciones encadenadas. Si ocurren más de un error, las excepciones se referencian a través de esta cadena. Recupera estas excepciones llamando al método SQLException.getNextException en la excepción que se lanzó.

1. Captura de excepciones

El siguiente método, printSQLException, muestra el SQLState, el código de error, la descripción del error y la causa (si la hay) contenidos en SQLException, así como cualquier otra excepción encadenada:

public static void printSQLException(SQLException ex) {

    for (Throwable e : ex) {
        if (e instanceof SQLException) {
            // Método implantado más adelante:
            if (!ignoraSQLException(((SQLException)e).getSQLState())) {

                // e.printStackTrace(System.err);

                System.err.println("Estado SQL: " + ((SQLException)e).getSQLState());

                System.err.println("Código error: " + ((SQLException)e).getErrorCode());

                System.err.println("Mensaje: " + e.getMessage());

                Throwable t = ex.getCause();
                while(t != null) { 
                    System.out.println("Causa: " + t);
                    t = t.getCause(); // LLamada recursiva.
                }
            }
        }
    }
}

Por ejemplo, si se invoca una llamada da una tabla que no existe la llamada al método ignoraSQLException, la salida será similar a la siguiente:

Estado SQL: 42Y55
Código error: 30000
Mensaje: 'DROP TABLE' cannot be performed on
'TESTDB.TABLAPRUEBA' because it does not exist.

En lugar de imprimir información de SQLException, podrías en su lugar primero recuperar el SQLState y procesar SQLException en consecuencia. Por ejemplo, el método ignoraSQLException devuelve true si el SQLState es igual al código 42Y55 (y estás utilizando Java DB como tu DBMS), lo que provoca que printSQLException ignore la SQLException:

public static boolean ignoraSQLException(String sqlState) {

    if (sqlState == null) {
        System.out.println("Este estado SQL no está declarado!");
        return false;
    }

    // X0Y32: Jar file already exists in schema
    if (sqlState.equalsIgnoreCase("X0Y32"))
        return true;

    // 42Y55: Table already exists in schema
    if (sqlState.equalsIgnoreCase("42Y55"))
        return true;

    return false;
}

2. Recuperación de warnings

Los objetos SQLWarning son una subclase de SQLException que gestiona los warnings de acceso a la base de datos.

Los warnings no detienen la ejecución de una aplicación, como lo hacen las excepciones; simplemente alertan al usuario de que algo no sucedió según lo planeado. Por ejemplo, una advertencia podría informar que un privilegio que intentaste revocar no se revocó. O una advertencia podría decir que ocurrió un error durante una desconexión solicitada.

Una advertencia se puede informar en un objeto Connection, un objeto Statement (incluidos los objetos PreparedStatement y CallableStatement) o un objeto ResultSet.

Cada una de estas interfaces (y sus clasesimplementadas) tiene un método getWarnings(), que se debe invocar para ver la primera advertencia informada en el objeto que llama.

Si getWarnings devuelve una advertencia, se llamar al método getNextWarning de SQLWarning en ella para obtener cualquier advertencia adicional. La ejecución de una instrucción borra automáticamente las advertencias de una instrucción anterior, por lo que no se acumulan.

Si se desea recuperar advertencias informadas en una orden, debe hacerse antes de ejecutar otra instrucción de cierre.

Por ejemplo, para acceder a cualquier advertencia informada en objetos Statement o ResultSet:

public static void getWarningsFromResultSet(ResultSet rs) throws SQLException {
    printWarnings(rs.getWarnings());
}

public static void getWarningsFromStatement(Statement stmt) throws SQLException {
    printWarnings(stmt.getWarnings());
}

public static void printWarnings(SQLWarning warning) throws SQLException {

    if (warning != null) {
        System.out.println("\n---Warning---\n");

    while (warning != null) {
        System.out.println("Mensaje: " + warning.getMessage());
        System.out.println("SQLState: " + warning.getSQLState());
        System.out.print("Código de error del proveedor: ");
        System.out.println(warning.getErrorCode());
        System.out.println("");
        warning = warning.getNextWarning();
    }
}

La advertencia más común es una advertencia de DataTruncation, una subclase de SQLWarning. Todos los objetos DataTruncation tienen un SQLState de 01004, lo que indica que hubo un problema al leer o escribir datos.

Los métodos de DataTruncation te permiten averiguar en qué columna o parámetro se truncaron los datos, si fue en una operación de lectura o escritura, cuántos bytes deberían haberse transferido y cuántos bytes se transfirieron realmente.

3. SQLExceptions categorizadas

Tu controlador JDBC podría lanzar una subclase de SQLException que corresponde a un SQLState común o a un estado de error común que no está asociado con un valor de clase SQLState específico, permitiendo concretar la excepción que produjo error:

  • SQLNonTransientException
  • SQLTransientException
  • SQLRecoverableException
  • BatchUpdateException.
  • RowSetWarning
  • SerialException
  • SQLClientInfoException
  • SQLWarning
  • SyncFactoryException
  • SyncProviderException

Consulta la última documentación de Javadoc del paquete java.sql.

classDiagram
    SQLException <|-- SQLNonTransientException
    SQLException <|-- SQLTransientException
    SQLException <|-- SQLRecoverableException
    SQLException <|-- RowSetWarning
    SQLException <|-- SQLWarning
    SQLWarning <|-- DataTruncation

4. Otras Subclases de SQLException

Las siguientes subclases de SQLException también pueden lanzarse:

  • BatchUpdateException se lanza cuando ocurre un error durante una operación de actualización por lotes. Además de la información proporcionada por SQLException, BatchUpdateException proporciona las cuentas de actualización para todas las declaraciones que se ejecutaron antes de que ocurriera el error.

  • SQLClientInfoException se lanza cuando no se pueden establecer una o más propiedades de información del cliente en una Connection. Además de la información proporcionada por SQLException, SQLClientInfoException proporciona una lista de propiedades de información del cliente que no se establecieron.

Última actualización: 23.09.2025

04. ResultSet


Recuperación y actualización con ResultSet

Un objeto ResultSet mantiene un cursor que apunta a su fila actual de datos.

https://docs.oracle.com/en/java/javase/21/docs/api/java.sql/java/sql/ResultSet.html

El siguiente método, showCafes, muestra el contenido de la tabla Cafes y demuestra el uso de objetos ResultSet y cursores:

public static void showCafes(Connection con) throws SQLException {
    String query = "select nome, idProveedor, precio, ventas, total from Cafe";
    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String nombreCafe = rs.getString("nome");
            int idProveedor = rs.getInt("idProveedor");
            float precio = rs.getFloat("precio");
            int ventas = rs.getInt("ventas");
            int total = rs.getInt("total");
            System.out.println(nombreCafe + ", " + idProveedor + ", " + precio +
                    ", " + ventas + ", " + total);
        }
    } catch (SQLException e) {
        // Manejo de excepciones
    }
}

Un objeto ResultSet es una tabla de datos que representa un conjunto de resultados de una base de datos, usualmente mediante la ejecución de una declaración que consulta la base de datos.

Por ejemplo, el método showCafes crea un ResultSet, rs, cuando ejecuta la consulta a través del objeto Statement, stmt.

Un objeto ResultSet se puede crear a través de cualquier objeto que implemente la interfaz Statement:

Se accede a los datos en un objeto ResultSet a través de un cursor, que no es un cursor de base de datos.

  • Un objeto ResultSet es un puntero que apunta a una fila de datos en el ResultSet.
  • Inicialmente, el cursor se sitúa antes de la primera fila.
  • El método ResultSet.next() mueve el cursor a la siguiente fila. Este método devuelve false si el cursor está situado después de la última fila.
  • ResultSet.next() se llama repetidamente al método ResultSet.next() con un bucle while para iterar a través de todos los datos en el ResultSet.

Veremos a continuación:

  • Interfaz ResultSet
  • Recuperación de valores de columnas de cada fila/registro.
  • Cursores
  • Actualización de Filas en Objetos ResultSet
  • Uso de Objetos Statement para Actualizaciones batch
  • Inserción de Filas en Objetos ResultSet

1. Interfaz ResultSet

La interfaz ResultSet dispone de métodos para recuperar y manipular los resultados de consultas ejecutadas. Pueden crearse objetos ResultSet con funcionalidades y características diferentes:

  • Tipo de cursor.
  • Concurrencia.
  • “Retención” del cursor.

A) Tipos de ResultSet

El primer argumento de los métodos createStatement, prepareStatement y prepareCall de Connection es el tipo de ResultSet.

El tipo de un objeto ResultSet determina el nivel de funcionalidad en dos aspectos:

  • Las formas en que se puede manipular el cursor (hacia adelante, hacia atrás, a una posición absoluta y así sucesivamente).
  • Cómo se reflejan los cambios concurrentes realizados en la fuente de datos (base de datos) mediante el objeto ResultSet: si se reflejan o no y cuándo se reflejan.

La sensibilidad de un objeto ResultSet está determinada por uno de los tres tipos diferentes de ResultSet:

  • TYPE_FORWARD_ONLY: el cursor se mueve solo hacia adelante, desde antes de la primera fila hasta después de la última fila. A veces se recupera fila a fila y no todos los resultados de una vez.
  • TYPE_SCROLL_INSENSITIVE: el cursor puede moverse hacia adelante y hacia atrás con respecto a la posición actual, y puede moverse a una posición absoluta. El ResultSet no es sensible a los cambios realizados en la base de datos mientras está abierto.
  • TYPE_SCROLL_SENSITIVE: el cursor puede moverse hacia adelante y hacia atrás con respecto a la posición actual, y puede moverse a una posición absoluta. El ResultSet refleja los cambios realizados en la base de datos subyacente mientras está abierto.

El tipo de ResultSet predeterminado es TYPE_FORWARD_ONLY.

Nota: No todas las bases de datos y controladores JDBC admiten todos los tipos de ResultSet. El método DatabaseMetaData.supportsResultSetType devuelve true si el tipo de ResultSet especificado es compatible y false en caso contrario.

B) Concurrencia de ResultSet (actualizable o no)

Es el segundo argumento de la createStatement, prepareStatement o prepareCall de Connection es la concurrencia.

La concurrencia de un objeto ResultSet determina qué nivel de funcionalidad de actualización se admite.

Hay dos niveles de concurrencia:

  • CONCUR_READ_ONLY: ResultSet no se puede actualizar.
  • CONCUR_UPDATABLE: ResultSet se puede actualizar.

La concurrencia predeterminada de ResultSet es CONCUR_READ_ONLY.

Nota: No todos los controladores JDBC y bases de datos admiten la concurrencia. El método DatabaseMetaData.supportsResultSetConcurrency devuelve true si el nivel de concurrencia especificado es compatible con el controlador y false en caso contrario.

Comprobación de si un ResultSet admite determinados niveles de concurrencia, tipo y actualización:

        try {
            if (con.getMetaData().supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE)) {
                System.out.println("Soporta TYPE_SCROLL_INSENSITIVE");
            } else {
                System.out.println("No soporta TYPE_SCROLL_INSENSITIVE");
            }
            if (con.getMetaData().supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE,
                    ResultSet.CONCUR_UPDATABLE)) {
                System.out.println("Soporta CONCUR_UPDATABLE");
            } else {
                System.out.println("No soporta CONCUR_UPDATABLE");
            }
        } catch (SQLException ex) {
            System.out.println("Error al obtener metadatos: " + ex.getMessage());
        }

El siguiente ejemplo muestra cómo usar un objeto ResultSet cuyo nivel de concurrencia es CONCUR_UPDATABLE:

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM Tabla1");
// rs será desplazable y no mostrará cambios realizados por otros.
// Será actualizable.

El método actualizarPrecios demuestra cómo usar un objeto ResultSet cuyo nivel de concurrencia es CONCUR_UPDATABLE:

  public void actualizarPrecios(float porcetaje) throws SQLException {
        try (Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
          ResultSet uprs = stmt.executeQuery("SELECT * FROM Cafe");
          while (uprs.next()) {
              float f = uprs.getFloat("precio");
              uprs.updateFloat("precio", f * porcetaje);
              uprs.updateRow();
          }
        } catch (SQLException e) {
          // Manejo de excepciones
        }
  }

C) Retención del Cursor (permanece abiertos cuando se llama al método commit)

Llamar al método Connection.commit (confirmar la transacción) puede cerrar los objetos ResultSet que se hayan creado durante la transacción actual.
En algunos casos, esto puede no ser el deseado. La propiedad holdability del ResultSet le da a la aplicación control sobre si los objetos ResultSet (cursores) se cierran cuando se llama a commit.

Las siguientes constantes de ResultSet se pueden suministrar a los métodos createStatement, prepareStatement y prepareCall de Connection:

  • HOLD_CURSORS_OVER_COMMIT: los cursores ResultSet no se cierran; permanecen abiertos cuando se llama al método commit. Los cursores retenidos pueden ser ideales si la aplicación utiliza principalmente objetos ResultSet de solo lectura.
  • CLOSE_CURSORS_AT_COMMIT: Los objetos ResultSet (cursores) se cierran cuando se llama al método commit. Cerrar cursores al llamar a este método puede dar mejor rendimiento para algunas aplicaciones.

La retención predeterminada del cursor varía según el SGBD.

Retención predeterminada del cursor

Nota: No todos los controladores JDBC y bases de datos admiten cursores retenibles y no retenibles. El método DatabaseMetaData.supportsResultSetHoldability devuelve true si el nivel de retención especificado es compatible con el controlador y false en caso contrario.

El método admiteRetencion muestra la retención predeterminada del cursor de los objetos ResultSet y si se admiten HOLD_CURSORS_OVER_COMMIT y CLOSE_CURSORS_AT_COMMIT:

public static void admiteRetencion(Connection conn) throws SQLException {
    
 DatabaseMetaData dbMetaData = conn.getMetaData();
    System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " +
            ResultSet.HOLD_CURSORS_OVER_COMMIT);
    System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
            ResultSet.CLOSE_CURSORS_AT_COMMIT);
    System.out.println("Retención predeterminada del cursor: " +
            dbMetaData.getResultSetHoldability());
    System.out.println("¿Admite HOLD_CURSORS_OVER_COMMIT? " +
            dbMetaData.supportsResultSetHoldability(
                    ResultSet.HOLD_CURSORS_OVER_COMMIT));
    System.out.println("¿Admite CLOSE_CURSORS_AT_COMMIT? " +
            dbMetaData.supportsResultSetHoldability(
                    ResultSet.CLOSE_CURSORS_AT_COMMIT));
}

2. Recuperación de valores (por filas)

ResultSet tiene métodos getXXX (por ejemplo, getBoolean(indice/nombre) y getLong(indice/nombre) para recuperar valores de columna desde la fila actual:

int getInt(int columnIndex/String columnName):
Date getDate(int columnIndex/String columnName);
String getString(int columnIndex/String columnName);
double getDouble(int columnIndex/String columnName);
 // ...
  • Se pueden recuperar valores utilizando el número de índice de la columna o el alias o nombre de la columna.
  • El índice de columna suele ser más eficiente. Las columnas se numeran a partir de 1.
  • Para máxima portabilidad, las columnas del conjunto de resultados dentro de cada fila deben leerse en orden de izquierda a derecha, y cada columna debe leerse solo una vez.

Por ejemplo, el siguiente método, showCafesPorIndice, recupera valores de columna por número:

public static void showCafesPorIndice(Connection con) throws SQLException {
    String query = "select nome, idProveedor, precio, ventas, total from Cafe";
    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String nombreCafe = rs.getString(1);
            int idProveedor = rs.getInt(2);
            float precio = rs.getFloat(3);
            int ventas = rs.getInt(4);
            int total = rs.getInt(5);
            System.out.println(nombreCafe + ", " + idProveedor + ", " + precio +
                    ", " + ventas + ", " + total);
        }
    } catch (SQLException e) {
        // Manejo de excepciones
    }
}

Los parámetros de String de todos los métodos de get no distinguen mayúsculas de minúsculas.

Una llamada a un método get con String y más de una columna tiene el mismo alias o nombre, devuelve el valor de la primera columna coincidente.

La opción de usar una cadena en lugar de un número entero está diseñada para utilizarse cuando las columnas tienen alias o nombres en la consulta SQL que generó el conjunto de resultados.
Para columnas que no se nombran explícitamente en la consulta (por ejemplo, select * from Cafe), es mejor emplear números de columnas.

getString con nombres únicos

Si se utilizan nombres de columna, se debe garantizar que se refieran de manera única a las columnas previstas mediante el uso de alias de columna, por medio de la cláusula SQL AS en la declaración SELECT.

getString con para recuperar otros tipos de datos

Nota: se recomienda el método getString para recuperar los tipos de SQL CHAR y VARCHAR, pero es posible recuperar cualquier tipo de SQL básicos con él. Obtener todos los valores con getString puede ser muy cómodo, pero convierte el valor numérico en un objeto String de Java.

Para tipos de datos no estándar SQL3 emplea getString.

3. Moviendo el cursor

Se accede a los datos en un objeto ResultSet a través de un cursor, que apunta a una fila en el objeto ResultSet.

Cuando se crea un objeto ResultSet, el cursor se sitúa antes de la primera fila.

El método showCafes mueve el cursor llamando al método ResultSet.next(). Hay otros métodos disponibles para mover el cursor:

  • next: mueve el cursor hacia adelante una fila. Devuelve true si el cursor está en una fila y false si se sitúa después de la última fila.
  • previous: mueve el cursor hacia atrás una fila. Devuelve true si el cursor está en una fila y false si el cursor está antes de la primera fila.
  • first: mueve el cursor a la primera fila en el objeto ResultSet. Devuelve true si el cursor está en la primera fila y false si el objeto ResultSet no contiene ninguna fila.
  • last: mueve el cursor a la última fila en el objeto ResultSet. Devuelve true si el cursor está en la última fila y false si el objeto ResultSet no contiene ninguna fila.
  • beforeFirst: sitúa el cursor al comienzo del objeto ResultSet, antes de la primera fila. Si el objeto ResultSet no contiene ninguna fila, este método no tiene efecto.
  • afterLast: sitúa el cursor al final del objeto ResultSet, después de la última fila. Si el objeto ResultSet no contiene ninguna fila, este método no tiene efecto.
  • relative(int rows): mueve el cursor en relación con su posición actual.
  • absolute(int row): sitúa el cursor en la fila especificada por el parámetro row.

La sensibilidad predeterminada de un ResultSet es TYPE_FORWARD_ONLY, lo que significa que no se puede desplazar. No se puede llamar a ninguno de estos métodos que mueven el cursor, excepto next, si el ResultSet no se puede desplazar.

4. Actualización de Filas con ResultSet

No se puede actualizar un objeto ResultSet con TYPE_FORWARD_ONLY.

Los ResultSet que pueden moverse (TYPE_SCROLL_SENSITIVE y TYPE_SCROLL_INSENSITIVE) (el cursor puede moverse hacia atrás o a una posición absoluta) pueden actualizarse.

  • Existen métodos de actualización de campos de ResultSet para todos los tipos de datos SQL: updateBoolean, updateByte, updateShort, updateInt, updateLong, updateFloat, updateDouble, updateBigDecimal, updateString, updateBytes, updateDate, updateTime, updateTimestamp, updateAsciiStream, updateBinaryStream, updateCharacterStream, updateObject.
  • Estos métodos actualizan el valor de un campo en la fila actual.
  • Una vez actualizado el valor de un campo, se debe llamar al método updateRow para que se haga efectivo el cambio en la base de datos.

El siguiente método, actualizaPrecios, multiplica la columna precio de cada fila por el porcentaje argumentado:

public void actualizaPrecios(float percentage) throws SQLException {
  try (Statement stmt =
    con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
    ResultSet uprs = stmt.executeQuery("SELECT * FROM Cafe");
    while (uprs.next()) {
      float f = uprs.getFloat("precio");
      uprs.updateFloat("precio", f * percentaje);
      uprs.updateRow();
    }
  } catch (SQLException e) {
    // Manejo de excepciones
  }
}

En el ejemplo:

  • El campo ResultSet.TYPE_SCROLL_SENSITIVE crea un objeto ResultSet cuyo cursor puede moverse.
  • El campo ResultSet.CONCUR_UPDATABLE crea un objeto ResultSet que se puede actualizar. Si no se especifica, el objeto ResultSet es de solo lectura.
  • El método ResultSet.updateFloat(campo, valor) actualiza la columna especificada (en este ejemplo, precio) con el valor float especificado en la fila donde está posicionado el cursor. ResultSet contiene varios métodos actualizadores que te permiten actualizar valores de columnas de varios tipos de datos. Para actualizar debe llamarse al método ResultSet.updateRow().
Última actualización: 23.09.2025

05. Actualizaciones por lotes con ResultSet


1. Órdenes Batch (por lotes) sobre un Statement

  • Los objetos Statement, PreparedStatement y CallableStatement tienen una lista de órdenes batch asociadas que podemos añadir con el [método addBatch(String s)](https://docs.oracle.com/en/java/javase/21/docs/api/java. sql/java/sql/Statement.html#addBatch(java.lang.String)).
  • No puede contener una declaración que produzca un ResultSet, como una declaración SELECT.
  • La lista de procesos Batch sólo puede contener declaraciones que produzcan una tarea de actualización
  • (UPDATE, INSERT, etc.) o de tipo DLL (CREATE TABLE, DROP TABLE, ALTER TABLE, etc.).
  • La lista se asocia con un objeto Statement en su creación (método addBatch) y está inicialmente vacía.
  • Se pueden añadir sentencias SQL a esta lista con el método addBatch y vaciarla con el método clearBatch.
  • Al terminar de añadir órdenes batch se invoca al método executeBatch para enviarlas todas a la base de datos para que se ejecuten como una unidad o lote.

Por ejemplo, el siguiente método, batchUpdate, añade cuatro filas a la tabla Cafe con una actualización por lotes (batch):

public void batchUpdate() throws SQLException {
  con.setAutoCommit(false); // deshabilita el modo de autocommit
  try (Statement stmt = con.createStatement()) {

    stmt.addBatch("INSERT INTO Cafe " +
                  "VALUES('Amaretto', 49, 9.99, 0, 0)");
    stmt.addBatch("INSERT INTO Cafe " +
                  "VALUES('Avellana', 49, 9.99, 0, 0)");
    stmt.addBatch("INSERT INTO Cafe " +
                  "VALUES('Amaretto_decaf', 49, 10.99, 0, 0)");
    stmt.addBatch("INSERT INTO Cafe " +
                  "VALUES('Avellana_decaf', 49, 10.99, 0, 0)");

    int[] updateCounts = stmt.executeBatch();
    con.commit();
  } catch (BatchUpdateException b) {
    // 
  } catch (SQLException ex) {
    // 
  } finally {
    con.setAutoCommit(true);
  }
}

La línea siguiente deshabilita el modo de autocommit para el objeto Connection con, de modo que la transacción NO se comprometerá ni se revertirá automáticamente cuando se llame al método executeBatch.

con.setAutoCommit(false);

Para permitir un manejo de errores correcto, siempre debes deshabilitar el modo de autocommit antes de comenzar una actualización batch.

Para enviar las órdenes SQL que se agregaron a la lista y que se ejecuten como un lote (st.executeBatch()):

int[] updateCounts = stmt.executeBatch();
  • stmt utiliza el método executeBatch para enviar el lote de INSERT, no el método executeUpdate, que envía sólo una orden y devuelve un sólo recuento de actualización.

  • El SGBD ejecuta las órdenes en el orden en que se agregaron a la lista, por lo que primero agregará la fila de valores para “Amaretto”, “Avellana”,…

  • Si los cuatro comandos se ejecutan correctamente, el método stmt.executeBatch() devuelve el recuento de actualización para cada orden SQL en el orden en que se ejecutó. Los recuentos de actualización que indican cuántas filas afectó cada comando se almacenan en el array updateCounts (puedes llamarle cómo quieras).

  • Si los cuatro comandos en el lote se ejecutan correctamente, updateCounts contendrá cuatro valores, en este caso con 1 porque una inserción afecta a una fila. La lista de comandos asociados con stmt ahora estará vacía porque los cuatro comandos agregados anteriormente se enviaron a la base de datos cuando stmt llamó al método executeBatch.

  • Se puede vaciar explícitamente esta lista de comandos en cualquier momento con el método clearBatch.

  • El método Connection.commit hace que el lote de actualizaciones en la tabla Cafe sea permanente. Este método debe llamarse de manera explícita, porque se deshabilitó el modo de autocommit previamente para esta conexión.

Volvemos a habilitar el modo autocommit para el objeto Connection:

con.setAutoCommit(true);

Es importante para que se vuelva a hacer commit de manera automática, y evitamos tener que llamar a commit.

2. Actualización batch parametrizada

También es posible realizar una actualización en lote parametrizada:

con.setAutoCommit(false);
PreparedStatement pstmt = con.prepareStatement(
                              "INSERT INTO Cafe VALUES( " +
                              "?, ?, ?, ?, ?)");
pstmt.setString(1, "Amaretto");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

pstmt.setString(1, "Avellana");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

// ... y así sucesivamente para cada nuevo
// tipo de café

int[] updateCounts = pstmt.executeBatch();
con.commit();
con.setAutoCommit(true);

3. Excepciones en listas batch

BatchUpdateException hereda de SQLException y obtenerse una excepción de tipo BatchUpdateException se invoca al método executeBatch si:

(1) Una de las declaraciones SQL que añadida al lote produce un ResultSet (por lo general, una consulta)

(2) Una de las declaraciones SQL no se ejecuta correctamente por alguna otra razón.

Recuerda que no se debe agregar una consulta (una declaración SELECT) a un lote de comandos SQL porque el método executeBatch, que devuelve un array de contador de actualizaciones, espera un recuento de actualización de cada declaración SQL que se ejecute correctamente:

  • INSERT INTO, UPDATE, DELETE, que devuelven el número de filas afectadas.
  • CREATE TABLE, DROP TABLE, ALTER TABLE, que devuelven 0. executeBatch.

Una BatchUpdateException contiene un array de recuentos de actualización similar al array devuelto por el método executeBatch. En ambos casos, los recuentos de actualización están en el mismo orden que los comandos que los produjeron.
Esto nos sirve para saber cuántos comandos en el lote se ejecutaron correctamente y cuáles son. Por ejemplo, si cinco comandos se ejecutaron correctamente, el array contendrá cinco números: el primero será el recuento de actualización para el primer comando, el segundo será el recuento de actualización para el segundo comando y así sucesivamente.

El método, printBatchUpdateException, imprime toda la información de SQLException más los recuentos de actualización contenidos en un objeto BatchUpdateException.
Dado que BatchUpdateException.getUpdateCounts devuelve un array de int, el código usa un bucle for para imprimir cada uno de los recuentos de actualización:

public static void printBatchUpdateException(BatchUpdateException b) {
  System.err.println("----BatchUpdateException----");
  System.err.println("SQLState:  " + b.getSQLState());
  System.err.println("Message:  " + b.getMessage());
  System.err.println("Vendor:  " + b.getErrorCode());
  System.err.print("Update counts:  ");
  int[] updateCounts = b.getUpdateCounts(); // array de int con los recuentos de actualización
  for (int i = 0; i < updateCounts.length; i++) {
    System.err.print(updateCounts[i] + "   ");
  }
}
Batch vs transacción

SQL Batch:

a) SQL Batch es una colección de sentencias que deben ejecutarse sin garantía de éxito o fracaso.

b) El procesamiento por lotes significa que las cosas se sitúan en una cola y se procesan cuando se alcanza cierta cantidad de elementos o cuando ha transcurrido cierto período de tiempo. Se puede deshacer/retroceder en esto.

SQL Transaction:

a) La Transacción SQL es una colección de sentencias que están garantizadas para tener éxito o fallar totalmente. Las transacciones no completarán la mitad de los comandos y luego fallarán en el resto; si uno falla, todos fallan.

b) La transacción es como un procesamiento en tiempo real que te permite deshacer/retroceder cambios.

En las TRANSACCIONES, es similar al lote, pero tienes la opción de “cancelarla”.

Por ejemplo, si el banco procesa tu solicitud de depósito y luego descubre que no tienes suficiente dinero en tu cuenta para cubrir el depósito, el banco puede cancelar la transacción y devolverte el cheque. El banco no puede hacer esto con el procesamiento por lotes.

Última actualización: 23.09.2025

06. Prepared Statement


Sentencias “preparadas” (PreparedStatement)

La interface PreparedStatement hereda de Statement y representa una sentencia SQL precompilada.

1. Características de PreparedStatement

  • En la mayoría de los casos se recomienda el uso de PreparedStatement para enviar sentencias SQL a la base de datos.
  • Una vez compilada, la sentencia preparada se puede ejecutar varias veces.
  • PreparedStatement es más eficientes que las sentencias Statement cuando se ejecutan varias veces, ya que la sentencia SQL se analiza y se compila solo una vez.
  • PreparedStatement también son útiles cuando se ejecutan consultas dinámicas, ya que permiten la separación de la sentencia SQL de los parámetros.

En cuanto al uso, la diferencia principal de un objeto PreparedStatement es que, a diferencia de un objeto Statement, se le proporciona una declaración SQL cuando se crea.
En la mayoría de los casos esta declaración SQL se envía al SGBD de inmediato, donde se compila. Como resultado, el objeto PreparedStatement contiene una declaración SQL que ha sido precompilada. Cuando se ejecuta el PreparedStatement, el SGB puede ejecutar la declaración SQL del PreparedStatement sin tener que compilarla primero.

  • Es la opción idónea para declaraciones SQL que toman parámetros, pues se puede usar la misma declaración y suministrar diferentes valores cada vez que se ejecuta.
  • La principal ventaja es que evita la inyección SQL, pues los parámetros se pasan por separado de la consulta SQL.
Inyeccion SQL

La inyección SQL es una técnica para explotar maliciosamente aplicaciones que utilizan datos proporcionados por el cliente en declaraciones SQL. Los atacantes engañan al motor SQL para ejecutar comandos no deseados al suministrar una entrada de cadena especialmente diseñada, obteniendo así acceso no autorizado a una base de datos para ver o manipular datos restringidos:

https://es.wikipedia.org/wiki/Inyecci%C3%B3n_SQL

Las sentencias preparadas siempre tratan los datos proporcionados por el cliente como contenido de un parámetro y nunca como parte de una declaración SQL.

Ejemplo:

public void updateVentas(HashMap<String, Integer> ventasPorSemana) throws SQLException {
    String updateString = "update Cafe set ventas = ? where nome = ?"; // Actualización de ventas.
    String updateStatement = "update Cafe set total = total + ? where nome = ?"; // Actualización del total.

    try (PreparedStatement updateVentas = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement)) {
        con.setAutoCommit(false);
        for (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
            updateVentas.setInt(1, e.getValue().intValue());
            updateVentas.setString(2, e.getKey());
            updateVentas.executeUpdate(); // Actualización de ventas.

            updateTotal.setInt(1, e.getValue().intValue());
            updateTotal.setString(2, e.getKey());
            updateTotal.executeUpdate(); // Incremento del total.
            con.commit();
        }
    } catch (SQLException e) {
        // Gestión de excepciones.
        if (con != null) {
            try {
                System.err.print("La transacción se está revirtiendo");
                con.rollback();
            } catch (SQLException excep) {
                // Gestión de excepciones.
            }
        }
    }
}

Método entrySet

2. Creación de PreparedStatement

Lo siguiente crea un objeto PreparedStatement que toma dos parámetros de entrada:

String updateString = "update Cafe " + "set ventas = ? where nome = ?";
PreparedStatement updateVentas = con.prepareStatement(updateString);

2.1. setTipoDato(columna, valor) de PreparedStatement

PreparedStatement tiene métodos para asignar valores a las ? de la sentencia SQL para cada tipo de dato.
Por ejemplo:

updateVentas.setInt(1, e.getValue().intValue());
updateVentas.setString(2, e.getKey());

clearParameters:

Después de darle un valor a un parámetro se retiene ese valor hasta que se restablece a otro valor o se llama al método clearParameters.

// cambia la columna ventas de Buñuelos
//fila a 100

updateVentas.setInt(1, 100); // Si no se cambia el valor, se mantendrá en 100.
updateVentas.setString(2, "Buñuelos");
updateVentas.executeUpdate();

// cambia la columna ventas de Tortitas americanas a 100
// (el primer parámetro se quedó en 100, y el segundo
// parámetro se restableció a "Tortitas americanas")

updateVentas.setString(2, "Tortitas americanas");
updateVentas.executeUpdate();

Uso de bucles para asignar valores:

Se puede facilitar la codificación mediante el uso de un bucle para asignar valores para los parámetros de entrada.

El método updateVentas utiliza un bucle for-each para establecer repetidamente valores en los objetos PreparedStatement updateVentas y updateTotal:

for (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
    updateVentas.setInt(1, e.getValue().intValue());
    updateVentas.setString(2, e.getKey());
    // ...
}

El método updateVentas toma un argumento, HashMap. Cada elemento en el argumento HashMap contiene el nombre y la cantidad vendida durante la semana actual.
El bucle for-each itera a través de cada elemento del HashMap.

3. Ejecución de sentencias con PreparedStatement: executeUpdate, executeQuery y execute.

Al igual que con los objetos Statement, para ejecutar un objeto PreparedStatement pude invocar:

  • executeQuery si la consulta devuelve solo un ResultSet (como una declaración SQL SELECT).
  • executeUpdate si la consulta no devuelve un ResultSet (como una declaración SQL UPDATE o INSERT).
  • execute si la consulta podría devolver más de un objeto ResultSet.

En updateVentas(HashMap<String, Integer>) son sentencias UPDATE, por lo que usa executeUpdate:

updateVentas.setInt(1, e.getValue().intValue());
updateVentas.setString(2, e.getKey());
updateVentas.executeUpdate();

updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate();
con.commit();

Nota: Al principio de updateVentas, el modo de confirmación automática se establece en false:

con.setAutoCommit(false);

En consecuencia, ninguna declaración SQL se confirma hasta que se llama al método commit.
Más adelante veremos cómo realizar transacciones.

4. Valores devueltos por executeUpdate

El valor de devuelto para executeUpdate es un valor int que indica cuántas filas de una tabla se actualizaron.
Por ejemplo:

updateVentas.setInt(1, 50);
updateVentas.setString(2, "Tortitas americanas");
int n = updateVentas.executeUpdate();
// n = 1 porque se cambió una fila.

Esa actualización afecta a una fila en la tabla, por lo que n es igual a 1.

Cuando el método executeUpdate se utiliza para ejecutar una declaración DDL (lenguaje de definición de datos), como en la creación de una tabla, devuelve el valor int de 0.
Por ejemplo:

// n = 0
int n = executeUpdate(crearTablaCafe); // Devuelve º filas afectadas.

Cuando el valor de devuelto por executeUpdate es 0, puede significar:

  1. La declaración ejecutada fue una declaración de actualización que no afectó a ninguna fila.
  2. La declaración ejecutada fue una declaración DDL.
Última actualización: 23.09.2025

07. Transacciones


Transacciones

En muchos casos de uso, es posible que desee ejecutar varias declaraciones SQL como una unidad de trabajo. Por ejemplo, supongamos que tiene una aplicación que actualiza los datos de una tabla y luego actualiza los datos de otra tabla. Desea asegurarse de que ambas actualizaciones se realicen correctamente o que no se realice ninguna de ellas.

La ejecución de varias declaraciones SQL como una unidad de trabajo se denomina transacción.

1. Desactivación de Auto-Commit

  • Por defecto, una conexión JDBC está en modo de auto-commit.
  • Cada sentencia SQL se trata como una transacción y se confirma automáticamente justo después de ejecutarse.
  • Para permitir que dos o más sentencias se agrupen en una transacción se debe desactivar el modo de auto-commit.
  • Ninguna sentencia SQL se confirma hasta que se llame explícitamente al método commit.
  • Todas las sentencias ejecutadas después de la llamada al método commit se incluyen en la transacción actual y se confirman juntas como una unidad.

(Para ser más preciso, el valor predeterminado es que una declaración SQL se confirme cuando se completa, no cuando se ejecuta. Sin embargo, en casi todos los casos, una declaración se completa y, por lo tanto, se confirma, justo después de ejecutarse.)

Desactivación de de auto-commit:

con.setAutoCommit(false);

2. Commit de transacciones

Ninguna declaración SQL se confirma hasta que se llame al método commit:

public void updateVentas(HashMap<String, Integer> ventasPorSemana) throws SQLException {

    try (PreparedStatement psVentas = con.prepareStatement("update Producto set ventas = ? where nome = ?");
         PreparedStatement psTotal = con.prepareStatement("update Producto set total = total + ? where nome = ?")) {
      con.setAutoCommit(false); // Deshabilita el modo de autocommit
      for (Map.Entry<String, Integer> e : ventasPorSemana.entrySet()) {
        psVentas.setInt(1, e.getValue().intValue());
        psVentas.setString(2, e.getKey());
        psVentas.executeUpdate();

        psTotal.setInt(1, e.getValue().intValue());
        psTotal.setString(2, e.getKey());
        psTotal.executeUpdate();
        con.commit(); // Confirmación 
      }
    } catch (SQLException e) {
      // Gestión de excepciones.
      if (con != null) {
        try {
          System.err.print("La transacción se está revirtiendo");
          con.rollback(); // Revierte la transacción
        } catch (SQLException excep) {
          // Gestión de excepciones.
        }
      }
    }
}
  • Las dos declaraciones preparadas psVentas y psTotal se confirman juntas cuando se llama al método commit().
  • Cada vez que se llama al método commit (ya sea automáticamente cuando se habilita el modo de auto-commit o explícitamente cuando se deshabilita), todos los cambios se vuelven permanentes.
  • La declaración con.setAutoCommit(true); habilita el modo de auto-commit, cada declaración se confirma automáticamente cuando se completa.
Desactivación y activación de auto-commit

Es recomendable desactivar el modo de auto-commit únicamente durante el modo de transacción.
De esta manera, se evita mantener bloqueos de base de datos para múltiples declaraciones, lo que aumenta la probabilidad de conflictos con otros usuarios.

3. Puntos de Guardado

El método Connection.setSavepoint establece un objeto Savepoint (punto de guardado) dentro de la transacción actual.
El método Connection.rollback se sobrecarga para aceptar un argumento Savepoint, un punto de guardado dentro de la transacción actual.

El siguiente método, Producto.modificarPreciosPorPoncertaje, aumenta el precio de un café en particular por un porcentaje, porcentaje. Sin embargo, si el nuevo precio es mayor que un precio especificado, precioMaximo, entonces el precio se revierte al precio original:

public void modificarPreciosPorPoncertaje(String nombre, float porcentaje, float precioMaximo) 
        throws SQLException {
    
  con.setAutoCommit(false);
  
  ResultSet rs = null;
  
  String precioQuery = "SELECT nombre, precio FROM Producto WHERE nombre = ?";
  String updateQuery = "UPDATE Producto SET precio = ? WHERE nombre = ?";
  
  try (PreparedStatement psPrecio = con.prepareStatement(precioQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
       PreparedStatement updatePrice = con.prepareStatement(updateQuery)) {
      
    Savepoint puntoSalvar = con.setSavepoint(); // Creación de punto de guardado
        
    psPrecio.setString(1, nombre);
    
    if (!psPrecio.execute()) { // Si no hay resultados
      System.out.println("No puedo encontrar el producto con nombre: " + nombre);
    } else {
      rs = psPrecio.getResultSet();
      rs.first(); // sitúa el cursor en la primera fila
        
      float precioAnterior = rs.getFloat("precio");
      float precioNuevo = precioAnterior + (precioAnterior * porcentaje);
      
      System.out.printf("Precio anterio de %s es $%.2f%n", nombre, precioAnterior);
      System.out.printf("Nuevo precio de %s es $%.2f%n", nombre, precioNuevo);
      
      System.out.println("Realizando actualización...");
      
      updatePrice.setFloat(1, precioNuevo);
      updatePrice.setString(2, nombre);
      updatePrice.executeUpdate();
      
      System.out.println("\nProducto después de actualización:");
      Producto.verTabla(con); // Ver tabla (debe implantarse)
        
      if (precioNuevo > precioMaximo) { // Si supera el máximo se hace un rollback al punto de guardado
          
        System.out.printf("El nuevo precio, $%.2f, es mayor que el precio máximo, $%.2f. " +
                          "Revertiendo la transacción...%n", precioNuevo, precioMaximo);
        con.rollback(puntoSalvar);
        
        System.out.println("\nProducto después de revertir:");
        
        Producto.viewTable(con); // Ver tabla (debe implantarse)
        
      }
      con.commit(); // Commit de la transacción
    }
  } catch (SQLException e) {
    // Gestión de excepciones.
  } finally {
    con.setAutoCommit(true);
  }
}

Se especifica que el cursor ResultSet generado por psPrecio se cierra cuando se llama al método commit. Ten en cuenta que si el SGBD no admite ResultSet.CLOSE_CURSORS_AT_COMMIT, ésta se ignora:

psPrecio = con.prepareStatement(consulta, ResultSet.CLOSE_CURSORS_AT_COMMIT);

El método comienza creando un Savepoint con la siguiente instrucción:

Savepoint puntoSalvar = con.setSavepoint();

El método verifica si el nuevo precio es mayor que el valor de precioMaximo. Si es así, el método deshace la transacción hasta el punto de guardado con la siguiente instrucción:

con.rollback(puntoSalvar);

Cuando el método realiza la transacción llamando al método Connection.commit, no comprometerá ninguna fila cuyo Savepoint asociado haya sido deshecho; comprometerá todas las demás filas actualizadas.

4. Liberación de Puntos de Guardado

El método Connection.releaseSavepoint toma un objeto Savepoint como parámetro y lo elimina de la transacción actual.

Después de que se ha liberado un punto de guardado, intentar hacer referencia en una operación de deshacer provoca que se lance una SQLException.

Cualquier punto de guardado que se haya creado en una transacción se libera automáticamente y se vuelve inválido cuando la transacción se confirma o cuando se revierte por completo la transacción.
Deshacer una transacción hasta un punto de guardado libera automáticamente y vuelve inválidos cualquier otro punto de guardado que se haya creado después del punto de guardado en cuestión.

5. Método rollback:

  • Llamar al método rollback termina una transacción y devuelve los valores que se modificaron a sus valores anteriores.
  • Si se intenta ejecutar una o más declaraciones en una transacción y se obtiene un SQLException, debe invocarse al método rollback para finalizar la transacción y comenzarla de nuevo.

Capturar un SQLException indica que hay errores, pero no te dice qué se ha comprometido o no.
Debido a que no no se puede saber qsi se ha completado alguna sentencia, llamar al método rollback es la única forma de estar seguro.

El método Producto.updateVentas demuestra una transacción e incluye un bloque catch que invoca al método rollback. Si la aplicación continúa y utiliza los resultados de la transacción, esta llamada al método rollback en el bloque catch evita el uso de datos posiblemente incorrectos.

6. Utilizando Transacciones en la integridad de los datos

  • Las transacciones pueden ayudar a preservar la integridad de los datos en una tabla.
  • El uso de transacciones proporciona algún nivel de protección contra conflictos que surgen cuando dos usuarios acceden a datos al mismo tiempo.
  • Para evitar conflictos durante una transacción, un SGBD utiliza bloqueos:
    • Mecanismo para bloquear el acceso de otros a los datos que está siendo accedido por la transacción. (Ten en cuenta que en el modo de autocommit, donde cada declaración es una transacción, los bloqueos se mantienen solo para una declaración).
  • Un bloqueo permanece vigente hasta que la transacción se confirma o se revierte.
Bloqueos

Los bloqueos pueden causar problemas de rendimiento.

Por ejemplo, un DBMS podría bloquear una fila de una tabla hasta que las actualizaciones en ella se hayan confirmado. El efecto de este bloqueo sería evitar que un usuario obtenga una lectura sucia, es decir, leer un valor antes de que se haga permanente. (Acceder a un valor actualizado que no se ha confirmado se considera una lectura sucia porque es posible que ese valor se revierta a su valor anterior. Si lees un valor que luego se revierte, habrás leído un valor no válido.)

Nivel de aislamiento de transacción

Cómo se establecen los bloqueos está determinado por lo que se llama un nivel de aislamiento de transacción, que puede variar desde no admitir transacciones en absoluto hasta admitir transacciones que imponen reglas de acceso muy estrictas.

Un ejemplo de un nivel de aislamiento de transacción es TRANSACTION_READ_COMMITTED, que no permitirá que se acceda a un valor hasta después de que se haya confirmado.
En otras palabras, si el nivel de aislamiento de la transacción se establece en TRANSACTION_READ_COMMITTED, el DBMS no permite lecturas sucias.

La interfaz Connection incluye cinco valores que representan los niveles de aislamiento de transacción:

Nivel de Aislamiento Transacciones Lecturas Sucias Lecturas No Repetibles Lecturas Fantasmales
TRANSACTION_NONE No admitido No aplicable No aplicable No aplicable
TRANSACTION_READ_COMMITTED Admitido Prevenido Permitido Permitido
TRANSACTION_READ_UNCOMMITTED Admitido Permitido Permitido Permitido
TRANSACTION_REPEATABLE_READ Admitido Prevenido Prevenido Permitido
TRANSACTION_SERIALIZABLE Admitido Prevenido Prevenido Prevenido
  • Una lectura no repetible ocurre cuando la transacción A recupera una fila, la transacción B actualiza posteriormente la fila, y la transacción A vuelve a recuperar la misma fila. La transacción A recupera la misma fila dos veces pero ve datos diferentes.

  • Una lectura fantasma ocurre cuando la transacción A recupera un conjunto de filas que cumplen con una condición dada, la transacción B inserta o actualiza posteriormente una fila de manera que ahora cumple con la condición en la transacción A, y la transacción A repite más tarde la recuperación condicional. La transacción A ahora ve una fila adicional. A esta fila se le denomina fantasma.

No necesitas hacer nada respecto al nivel de aislamiento de la transacción; puedes usar el predeterminado para el SGBD empleado.
El nivel de aislamiento de transacción predeterminado depende del SGBD. Por ejemplo, para Java DB, es TRANSACTION_READ_COMMITTED. JDBC te permite averiguar a qué nivel de aislamiento de transacción está configurado tu DBMS (usando el método getTransactionIsolation de Connection) y también te permite establecerlo en otro nivel (usando el método setTransactionIsolation de Connection).

Niveles de aislamiento de transacción

Nota: es muy probable que un controlador JDBC no admita todos los niveles de aislamiento de transacción.
Si un controlador no admite el nivel de aislamiento especificado en una invocación de setTransactionIsolation, el controlador puede sustituir un nivel de aislamiento de transacción más alto y restrictivo.
Si un controlador no puede sustituir un nivel de transacción más alto, se produce una SQLException.

Utiliza el método DatabaseMetaData.supportsTransactionIsolationLevel para determinar si el controlador admite o no un nivel dado.

Última actualización: 23.09.2025

08. Claves Generadas


1. Introducción

Muchas veces necesitamos obtener el valor de una clave primaria generada automáticamente después de insertar un registro en la base de datos. Mediante JDBC podemos obtener el valor de la clave primaria generada automáticamente después de insertar un registro en la base de datos.

2. Configuración y creación de la tabla

A m odo de ejemplo, poder ejecutar consultas SQL, utilizaremos una base de datos H2 en memoria:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
</dependency>

También utilizaremos una tabla muy sencilla con dos columnas:

public class JdbcInsertId {

    private static Connection connection;
    
    public static void inicio() throws Exception {
        connection = DriverManager.getConnection("jdbc:h2:mem:dbConClaves", "sa", "");
        connection.createStatement().execute("create table Persona (id bigint auto_increment, nome varchar(255))");
    }
    
    public static void borrado() throws SQLException {
        connection.createStatement()
          .execute("drop table Persona");
        connection.close();
    }

    // ...
}

Que se conecta a la base de datos en memoria “dbConClaves” y crea una tabla llamada “Persona”.

3. Statement.RETURN_GENERATED_KEYS y getGeneratedKeys()

Una forma de obtener las claves después de la generación automática es pasar Statement.RETURN_GENERATED_KEYS al método prepareStatement():

String QUERY = "insert into Persona (nome) values (?)";
try (PreparedStatement statement = conexion.prepareStatement(QUERY, Statement.RETURN_GENERATED_KEYS)) {
    statement.setString(1, "Otto");
    int filasInsertadas = statement.executeUpdate();
    if (filasInsertadas > 0) {
        // ...
    } else {
        // ...
    }

    // ...
} catch (SQLException e) {
    // manejar la excepción relacionada con la base de datos de manera apropiada
}

Después de preparar y ejecutar la consulta, se puede llamar al método getGeneratedKeys() en PreparedStatement para obtener el id:

try (ResultSet claves = statement.getGeneratedKeys()) {
    if(claves.next()) {
        // ...
        long id = claves.getLong(1);
    } else {
        // ...
    }
}

Que llama al método next() para mover el cursor del resultado con las claves generadas.
El método getLong() obtiene obtener la primera columna como long.

Además, también es posible utilizar la misma técnica con Statements normales:

try (Statement statement = conexion.createStatement()) {
    String query = "insert into Persona (nome) values ('Otto')";
    int filasAfectadas = statement.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
    if(filasAfectadas > 0) {
        // ...
    } else {
        // ...
    }

    try (ResultSet claves = statement.getGeneratedKeys()) {
        if(claves.next()) {
            // ...
            long id = claves.getLong(1);
            // ...
        } else {
            // ...
        }
    }
}

Debe emplearse try-with-resources de manera extensiva para permitir que el compilador limpie los resultados.

4. Devolver Columnas Específicas

Podemos hacer que devuelva columnas específicas después de emitir una consulta. Para hacer eso, solo tenemos que pasar un array de nombres de columna:

try (PreparedStatement statement = conexion.prepareStatement(QUERY, new String[] { "id" })) {
    statement.setString(1, "Otto");
    int filasAfectadas = statement.executeUpdate();
    if (filasAfectadas > 0) {
        // ...
    } else {
        // ...
    }

    // ...
}

En el ejemplo anterior devuelve el valor de la columna id después de ejecutar la consulta dada.

Similar al ejemplo anterior, podemos obtener el id después:

try (ResultSet claves = statement.getGeneratedKeys()) {
    if(claves.next()) {
        // ...
        long id = claves.getLong(1);
    } else {
        // ...
    }
}

Podemos utilizar el mismo enfoque con Statements simples:

try (Statement statement = conexion.createStatement()) {
    int filasAfectadas = statement.executeUpdate("insert into Persona (nome) values ('Otto')", new String[] { "id" });
        if(filasAfectadas > 0) {
            // ...
        } else {
            // ...
        }

    try (ResultSet claves = statement.getGeneratedKeys()) {
        if(claves.next()) {
            // ...
            long id = claves.getLong(1);
            // ...
        } else {
            // ...
        }
    }
}
Última actualización: 23.09.2025

09. Objectos grandes


1. Introducción

Los objetos grandes (LOB, Large Objects) son objetos de datos que pueden tener un tamaño variable y que se almacenan en una base de datos. Se utilizan para almacenar datos como imágenes, sonidos, videos y documentos de texto.

Por lo general, las bases de datos almacenan los datos de la siguiente forma: las columnas se agrupan en filas que, a su vez, se apilan en bloques de datos. La información en cada bloque de datos está asociada a una fila y los bloques de datos consumen así menos espacio en la base de datos.

  • Las bases de datos tratan de otro modo los objetos de datos de mayor tamaño. Los LOB superan en tamaño a las entradas convencionales de las bases de datos y no se encuentran estructurados.
  • En la mayoría de los casos, se almacenan en un lugar distinto.
    La base de datos sólo crea en la posición que corresponda una referencia a la ubicación de almacenamiento.

Existen dos tipos de LOB:

  • BLOB: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario.
  • CLOB: un CLOB (Character Large Objects) almacena cadenas largas de caracteres. Es un término acuñado por los desarrolladores de la base de datos de Oracle.

Nota: otros sistemas de gestión de bases de datos utilizan también otros términos para denominar los objetos grandes: MySQL/MariaDB y PostgreSQL los denominan TEXT.

Tipos de datos para almacenar objetos LOB (Large Objects) tanto binarios como de texto en diferentes Sistemas de Gestión de Bases de Datos (SGBDR), junto con sus tamaños típicos:

  1. MariaDB:

    • Binario (BLOB): BLOB o LONGBLOB (hasta 4 GB).
    • Texto (CLOB): TEXT o LONGTEXT (hasta 4 GB).
  2. H2:

    • Binario (BLOB): BLOB (64TB)
    • Texto (CLOB): CLOB (64TB).
  3. SQLite:

    • Binario (BLOB): No hay un tipo específico para BLOB, se pueden usar tipos de datos TEXT o BLOB (hasta 2 GB).
    • Texto (CLOB): No hay un tipo específico para CLOB, se pueden usar tipos de datos TEXT o BLOB (hasta 2 GB).
  4. PostgreSQL:

    • Binario (BLOB): BYTEA o OID (hasta 1 GB).
    • Texto (CLOB): TEXT o VARCHAR (sin límite declarado, prácticamente limitado por el tamaño de la tabla).
  5. Oracle:

    • Binario (BLOB): BLOB (hasta 128 TB).
    • Texto (CLOB): CLOB (hasta 128 TB).
  6. MS SQL Server:

    • Binario (BLOB): VARBINARY(MAX) o IMAGE (hasta 2 GB en VARBINARY(MAX) y hasta 4 GB en IMAGE).
    • Texto (CLOB): VARCHAR(MAX) o TEXT (hasta 2 GB en VARCHAR(MAX) y hasta 2 GB en TEXT).

MariaDB y MySQL tienen cuatro tipos de datos de texto y LOB:

  • TINYTEXT: un texto de longitud máxima de 255 caracteres.
  • TEXT: un texto de longitud máxima de 65.535 caracteres (64KB).
  • MEDIUMTEXT: un texto de longitud máxima de 16.777.215 caracteres.
  • LONGTEXT: un texto de longitud máxima de 4.294.967.295 caracteres (4GB).
  • BLOB: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario (6GB).
  • MEDIUMBLOB: un BLOB de longitud máxima de 16.777.215 bytes.
  • LONGBLOB: un BLOB de longitud máxima de 4.294.967.295 bytes. (4GB).
  • TINYBLOB: un BLOB de longitud máxima de 255 bytes.

H2 tiene dos tipos de datos de texto:

  • CLOB/CHARACTER LARGE OBJECT: un texto de longitud máxima de 2GB.
  • BLOB/BINARY LARGE OBJECT: un BLOB es un tipo de dato que almacena un elemento grande de datos en código binario.
  • BINARY: un BLOB de longitud máxima de 2GB.
  • VARBINARY: un BLOB de longitud máxima de 2GB.
  • LONGVARBINARY: un BLOB de longitud máxima de 2GB.

1. Uso de LOB (Objetos de Gran Tamaño)

Los objetos grandes Java, como Blob, Clob y NClob pueden gestionarse desde Java sin tener que traer los datos del servidor de la base de datos al cliente.

Muchas implementaciones representan una instancia de estos tipos de datos con un localizador (puntero) al objeto en la base de datos.

Debido a que un objeto BLOB, CLOB o NCLOB puede ser muy grande, el uso de punteros mejora el rendimiento. Sin embargo, algunas implementaciones gestionan (y cargan) completamente objetos grandes en cliente.

Para traer un BLOB, CLOB o NCLOB de SQL al programa cliente, se emplean métodos en las interfaces de Java Blob, Clob y NClob.

1. Añadir un CLOB la Base de Datos

La interface PreparedStatement tiene métodos para asignar valores a las ? de la sentencia SQL para cada tipo de dato. Para CLob se utiliza el método setClob:

void setClob(int parameterIndex, Reader reader);
void setClob(int parameterIndex, Reader reader, long length);
void setClob(int parameterIndex, Clob x);
// o
void setCharacterStream(int parameterIndex, Reader reader);
void setCharacterStream(int parameterIndex, Reader reader, int length); 
void setCharacterStream(int parameterIndex, Reader reader, long length); 
//Insertar valores
PreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, descripcion) VALUES (?, ?)");
pstmt.setString(1, "nombre de ejemplo");
pstmt.setClob(2, Files.newBufferedReader(Paths.get("E:\\descripcion.txt")));
pstmt.executeUpdate();

Clob:

El siguiente extracto de addDescripcionProducto agrega un valor SQL CLOB a la tabla Producto. El objeto Java Clob clobDescripcion contiene el contenido del archivo especificado por nomeArquivo.

public void addDescripcionProducto(String nome, String nomeArquivo) throws SQLException {
    // Cfreación del objeto Clob:
    Clob clobDescripcion = this.con.createClob();
    
    try (PreparedStatement pstmt = this.con.prepareStatement("INSERT INTO Producto VALUES(?,?)");
      Writer clobWriter = clobDescripcion.setCharacterStream(1);){ 
        // setCharacterStream devuelve un objeto Writer y recibe un entero que indica la posición inicial del Clob.
        
      String str = this.readFile(nomeArquivo, clobWriter); // Lee el conteido del archivo. 
      System.out.println("Escribo el texto: " + clobWriter.toString());
      
      // Si el archivo es demasiado grande, se puede escribir en el Clob en trozos.
      clobDescripcion.setString(1, str);
      
      System.out.println("Longitud del clob: " + clobDescripcion.length());
      pstmt.setString(1, nome);
      pstmt.setClob(2, clobDescripcion); // Se añade el Clob al PreparedStatement.
      pstmt.executeUpdate();
      
    } catch (SQLException sqlex) {
      // Gestión de excepciones.
    } catch (Exception ex) {
      System.out.println("Excepción no esperada: " + ex.toString());
    }
}

private String readFile(String nomeArquivo, Writer writer) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(nomeArquivo))) {
            String nextLine = "";
            StringBuffer sb = new StringBuffer();
            while ((nextLine = br.readLine()) != null) {
                System.out.println("Escribiendo: " + nextLine);
                writer.write(nextLine);
                sb.append(nextLine);
            }
            // Convertir el contenido en una cadena
            String datosClob = sb.toString();
            // devolución de los datos.
            return datosClob;
        }
}

a) Creación de un objeto Clob:

Clob clobDescripcion = this.con.createClob();

b) Recuperación del flujo (en este caso, un objeto Writer llamado clobWriter) que se utiliza para escribir un flujo de caracteres en el objeto Java Clob clobDescripcion. El método readFile escribe este flujo de caracteres; el flujo proviene del archivo especificado por la cadena nomeArquivo. El argumento del método 1 indica que el objeto Writer comenzará a escribir el flujo de caracteres al principio del valor Clob:

Writer clobWriter = clobDescripcion.setCharacterStream(1);

El método readFile lee el archivo línea por línea especificado por el archivo nomeArquivo y lo escribe en el objeto Writer especificado por writer:

private String readFile(String nomeArquivo, Writer writer) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(nomeArquivo))) {
        String nextLine = "";
        StringBuffer sb = new StringBuffer();
        while ((nextLine = br.readLine()) != null) {
            System.out.println("Escribiendo: " + nextLine);
            writer.write(nextLine);
            sb.append(nextLine);
        }
        // Convertir el contenido en una cadena
        String clobData = sb.toString();
        // Devolver los datos.
        return clobData;
    }
}

c) Creación de un objeto PreparedStatement pstmt que inserta el objeto Java Clob clobDescripcion en Producto:

String sql = "INSERT INTO Producto VALUES(?,?)";
Clob clobDescripcion = this.con.createClob();
try (PreparedStatement pstmt = this.con.prepareStatement(sql);
  // ...
  ) {
  // ...
  pstmt.setString(1, nome);
  pstmt.setClob(2, clobDescripcion);
  pstmt.executeUpdate();
  // ...
}

2. Recuperando valores CLOB

El método getDescripcion recupera el valor SQL CLOB almacenado en la columna descripcion de Producto de la fila cuyo valor de columna nome es igual al valor de la cadena especificada por el parámetro nome:

public String getDescripcion(String nome, int numeroCaracteres) throws SQLException {

    String descripcion = null;
    Clob clobDescripcion = null;
    String sql = "select descripcion from Producto where nome = ?";

    try (PreparedStatement pstmt = this.con.prepareStatement(sql)) {
      pstmt.setString(1, nome);
      ResultSet rs = pstmt.executeQuery();
      if (rs.next()) {
        clobDescripcion = rs.getClob(1);
        System.out.println("Lonxitude do Clob: " + clobDescripcion.length());
      }
      descripcion = clobDescripcion.getSubString(1, numeroCaracteres);
    } catch (SQLException sqlex) {
      // Tratamiento de excepciones.
    } catch (Exception ex) {
      System.out.println("Excepción: " + ex.toString());
    }
    return descripcion;
}

Recupera el valor Java Clob del objeto ResultSet rs:

clobDescripcion = rs.getClob(1);

Recuperación de una subcadena del objeto clobDescripcion.
La subcadena comienza en el primer carácter del valor de clobDescripcion y tiene hasta el número de caracteres consecutivos especificados en numeroCaracteres, donde numeroCaracteres es un entero.

descripcion = clobDescripcion.getSubString(1, numeroCaracteres);

3. Agregando y recuperado Objetos BLOB

setBlob de PreparedStatement

PreparedStatement tiene métodos para asignar valores a las ? de la sentencia SQL para cada tipo de dato.

void setBlob(int parameterIndex, InputStream inputStream)
void setBlob(int parameterIndex, InputStream inputStream, long length)
void setBlob(int parameterIndex, Blob x)
// o
void setBinaryStream(int parameterIndex, InputStream x);
void setBinaryStream(int parameterIndex, InputStream x, int length);
 void setBinaryStream(int parameterIndex, InputStream x, long length);

Vamos a verlo con el método setBlob que recoge un objeto Blob.

Agregar y recuperar objetos SQL BLOB es similar a agregar y recuperar objetos CLOB.
Se precisa crear un blob, para ello se utiliza el método createBlob.

Y el método el método Blob.setBinaryStream para recuperar un objeto OutputStream para escribir el valor BLOB.

El siguiente extracto de addImagenProducto agrega un valor SQL BLOB a la tabla Producto. El objeto Java Blob blobImagen contiene el contenido del archivo especificado por nomeArquivo.

public void addImagenProducto(String nome, String nomeArquivo) throws SQLException {
    // Creación del objeto Blob:
    Blob blobImagen = this.con.createBlob();
    
    try (PreparedStatement pstmt = this.con.prepareStatement("INSERT INTO Producto VALUES(?,?)");
      OutputStream blobOutputStream = blobImagen.setBinaryStream(1);){ 
        // setBinaryStream devuelve un objeto OutputStream y recibe un entero que indica la posición inicial del Blob.
        
      byte[] bytes = this.readFile(nomeArquivo, blobOutputStream); // Lee el conteido del archivo. 
      System.out.println("Escribo el texto: " + blobOutputStream.toString());
      
      // Si el archivo es demasiado grande, se puede escribir en el Blob en trozos.
      blobImagen.setBytes(1, bytes);
      
      System.out.println("Longitud del blob: " + blobImagen.length());
      pstmt.setString(1, nome);
      pstmt.setBlob(2, blobImagen); // Se añade el Blob al PreparedStatement.
      pstmt.executeUpdate();
      
    } catch (SQLException sqlex) {
      // Gestión de excepciones.
    } catch (Exception ex) {
      System.out.println("Excepción no esperada: " + ex.toString());
    }
}

setBinaryStream de PreparedStatement

Otro modo de realizarlo es por medio del método setBinaryStream:

Este método esta sobrecargado y se puede utilizar de varias formas:

public void setBinaryStream(int index, InputStream is)
public void setBinaryStream(int index, InputStream is, int length)
public void setBinaryStream(int index, InputStream is, long length)

Por ejemplo:

//Insertar valores
PreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, imagen) VALUES (?, ?)");
pstmt.setString(1, "imagen de ejemplo");
FileInputStream fin = new FileInputStream("E:\imagenes\otto.jpg");
pstmt.setBinaryStream(2, fin);
pstmt.execute();

setBytes de PreparedStatement

Otra forma de realizarlo es por medio del método setBytes:

void setByte(int parameterIndex, byte x)
void setBytes(int parameterIndex, byte[] x)

Ejemplo:

//Insertar valores
PreparedStatement pstmt = con.prepareStatement("INSERT INTO Table(nombre, imagen) VALUES (?, ?)");
pstmt.setString(1, "imagen de ejemplo");
pstmt.setBytes(2, Files.readAllBytes(Paths.get("E:\imagenes\otto.jpg")));
pstmt.execute();

4. Liberando Recursos Retenidos por Objetos Grandes

Los objetos Java Blob, Clob y NClob siguen siendo válidos durante al menos la duración de la transacción en la que se crearon. Esto podría resultar en que una aplicación se quede sin recursos durante una transacción de larga duración. Las aplicaciones pueden liberar los recursos de Blob, Clob y NClob invocando su método free:

Clob aClob = con.createClob();
int numWritten = aClob.setString(1, val);
aClob.free();
Última actualización: 23.09.2025

11. RowSet


Introducción

Un objeto JDBC RowSet hereda de la interface ResultSet, almacenando datos en forma de tabla de una manera que lo hace más flexible y fácil de usar que un ResultSet.

El API ha definido 5 interfaces RowSet para algunos de los usos más comunes de un RowSet con implementaciones de referencia estándar disponibles para estas interfaces RowSet. Las implementaciones de referencia estándar son JdbcRowSet, CachedRowSet, , WebRowSet, JoinRowSet y FilteredRowSet.

  • JdbcRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC.

  • CachedRowSet es un objeto RowSet que almacena datos en caché en la memoria del cliente.

  • WebRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y puede escribirse en un flujo de datos XML.

  • JoinRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y puede unirse a datos de varias fuentes de datos.

  • FilteredRowSet es un objeto RowSet que proporciona conectividad de base de datos a través de una conexión JDBC y puede filtrar filas.

  • Es posible escribir las implementaciones personalizadas de la interface javax.sql.RowSet, o heredar de las implementaciones de las cinco interfaces RowSet.

  • Para la mayoría de los casos las implementaciones de referencia estándar cubren todas las necesidades.

1. La interface RowSet

RowSet es una interfaz en Java que se encuentra en el módulo java.sql (no confundir RowSet con ResultSet).

RowSet está en paquete javax.sql, mientras que ResultSet está en java.sql.

La instancia de RowSet es el componente JavaBean porque tiene propiedades y un mecanismo de notificación de JavaBean.
Se introdujo en JDK5. Un JDBC RowSet proporciona una forma de almacenar los datos en forma tabular.
Hace que los datos sean más flexibles y más fáciles que un ResultSet.
La conexión entre el objeto RowSet y la fuente de datos se mantiene durante todo su ciclo de vida.

COmo hemos comentado, RowSets se clasifican en cinco categorías según cómo estén implementados, que se enumeran a continuación:

  • JdbcRowSet
  • CachedRowSet
  • WebRowSet
  • FilteredRowSet
  • JoinRowSet

2. Ventajas de RowSet

  • Es fácil y flexible de usar.

  • Por defecto, es desplazable y puede actualizarse, mientras que ResultSet, por defecto, solo es válido para operaciones hacia adelante y de solo lectura.

  • Es un componente JavaBean (tiene propiedades y eventos), por lo que es fácil de usar en cualquier entorno y herramienta de desarrollo, además de ser compatible con la notificación de JavaBean:

    • Las propiedades de un objeto RowSet se pueden establecer y obtener mediante los métodos set y get y se ven en lo IDEs.
    • Los eventos de un objeto RowSet se pueden registrar y recibir mediante los métodos add y remove y se ven en lo IDEs:
    • Los eventos se producen cuando se mueve el cursor, se actualiza una fila, se elimina una fila, se inserta una fila, etc.
      • RowSetEvent se genera cuando se produce un cambio en el objeto RowSet.
      • RowSetMetaDataEvent se genera cuando se produce un cambio en el objeto RowSetMetaData.
      • RowSetWarningEvent se genera cuando se produce un cambio en el objeto RowSetWarning.
  • Puede ser serializado.

  • La interfaz JDBC RowSet hereda de RowSet. Es un contenedor para el objeto ResultSet que añade características.

Métodos comunes a todas las implementaciones de RowSet:

3. La interfaz JdbcRowSet

public interface JdbcRowSet extends RowSet, Joinable

Para conectar RowSet con la base de datos, la interfaz RowSet proporciona métodos para configurar las propiedades de JavaBean:

void setURL(String url);
void setUserName(String user_name);
void setPassword(String password);

Sólo se precisa crear un objeto JdbcRowSet:

JdbcRowSet rowSet = RowSetProvider.newFactory().createJdbcRowSet();

// 1. Base de datos Oracle
rowSet.setUrl("jdbc:oracle:thin:@localhost:1521:xe");

// 2. El nombre de usuario se establece personalmente como - root
rowSet.setUsername("root");

// 3. La contraseña se establece personalmente como - pass
rowSet.setPassword("pass");

// 4. Consulta
rowSet.setCommand("select * from Students");

Implementación

Supongamos que tenemos una tabla llamada Estudiante en la base de datos con datos:

idEstudiante nombre nota
1 otto 92
2 xoel 90
3 marco 80
4 xoan 82

Implementación de JdbcRowSet y recuperación de los registros:

// Programa Java para ilustrar RowSet en JDBC

// Importación de base de datos
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import javax.sql.RowSetEvent;
import javax.sql.RowSetListener;
import javax.sql.rowset.JdbcRowSet;
import javax.sql.rowset.RowSetProvider;

// Clase principal
class RowSetDemo {

	// Método principal
	public static void main(String args[]) {

		// Bloque Try para verificar excepciones
		try {

			// Carga y registro de controladores
			Class.forName("oracle.jdbc.driver.OracleDriver");

			// Creación de un RowSet
			JdbcRowSet rowSet = RowSetProvider.newFactory().createJdbcRowSet();

			// Configuración de URL, nombre de usuario, contraseña
			rowSet.setUrl("jdbc:oracle:thin:@localhost:1521:xe");
			rowSet.setUsername("root");
			rowSet.setPassword("pass");

			// Creación de una consulta
			rowSet.setCommand("select * from Estudiante");

			// Ejecución de la consulta
			rowSet.execute();

			// Procesamiento de los resultados
			while (rowSet.next()) {

				// Comandos de impresión y visualización
				System.out.println("idEstudiante: " + rowSet.getInt(1));
				System.out.println("nombre: " + rowSet.getString(2));
				System.out.printf("nota: %.1f", rowSet.geInt(3)/10.);
			}
		}

		// Bloque catch para manejar las excepciones
		catch (Exception e) {

			// Imprimir y mostrar la excepción junto con
			// el número de línea usando el método printStackTrace()
			e.printStackTrace();
		}
	}
}

Salida:

idEstudiante: 1
nombre: otto 
nota: 9.2
idEstudiante: 2
nombre: xoel  
nota: 9.0
idEstudiante: 3
nombre: marco
nota: 8.0
idEstudiante: 4
nombre: xoan
nota: 8.2

Métodos principales de la interfaz JdbcRowSet que no heredan de RowSet

Ejemplo de selección parametrizada JdbcRowSet:

     JdbcRowSetImpl jrs = new JdbcRowSetImpl();
        jrs.setCommand("SELECT * FROM TITLES WHERE TYPE = ?");
        jrs.setURL("jdbc:myDriver:myAttribute");
        jrs.setUsername("cervantes");
        jrs.setPassword("sancho");
        jrs.setString(1, "BIOGRAPHY");
        jrs.execute();
Última actualización: 23.09.2025

15. Ejercicios y archivos de la unidad

Boletines de ejercicios

Ejercicios

Archivos de la unidad

La base de datos de ejemplo para los ejercicios de esta unidad es la siguiente (Está incorporado en el fichero anexo):

-- PUBLIC."Book" definition

-- Drop table

-- DROP TABLE PUBLIC."Book";

CREATE TABLE PUBLIC."Book" (
	"idBook" INTEGER NOT NULL AUTO_INCREMENT,
	"isbn" CHARACTER VARYING(13) NOT NULL,
	"titulo" CHARACTER VARYING(255) NOT NULL,
	"autor" CHARACTER VARYING(255),
	"anho" INTEGER,
	"disponible" BOOLEAN DEFAULT TRUE,
	"portada" BINARY LARGE OBJECT,
	CONSTRAINT BOOK_PK PRIMARY KEY ("idBook")
);
CREATE UNIQUE INDEX "IdBookPK" ON PUBLIC."Book" ("idBook");
CREATE INDEX "IdxBookISBN" ON PUBLIC."Book" ("isbn");
CREATE INDEX "IdxBookTitle" ON PUBLIC."Book" ("titulo");
CREATE UNIQUE INDEX PRIMARY_KEY_1 ON PUBLIC."Book" ("idBook");

-- PUBLIC."Contido" definition

-- Drop table

-- DROP TABLE PUBLIC."Contido";

CREATE TABLE PUBLIC."Contido" (
	"idContido" INTEGER NOT NULL AUTO_INCREMENT,
	"idBook" INTEGER NOT NULL,
	"contido" CHARACTER LARGE OBJECT,
	CONSTRAINT "Contido_PK" PRIMARY KEY ("idContido"),
	CONSTRAINT FK_ID_BOOK PRIMARY KEY ("idBook","idBook")
);
CREATE INDEX FK_ID_BOOK_INDEX_9 ON PUBLIC."Contido" ("idBook");
CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC."Contido" ("idContido");


-- PUBLIC."Contido" foreign keys

ALTER TABLE PUBLIC."Contido" ADD CONSTRAINT FK_ID_BOOK FOREIGN KEY ("idBook") REFERENCES PUBLIC."Book"("idBook") ON DELETE CASCADE ON UPDATE CASCADE;
Bases de datos

Las bases de datos están incorporadas en el fichero, así como los datos y los textos e imágenes de ejemplo:

Proyectos

Ejercicio 1. Gestión Biblioteca

Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección de libros.

Para ello, vamos a crear:

  • Clase Book: representa la entidad libro.
  • Clase BookDAO: permite realizar operaciones CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.
  • Clase ConnectionManager: para la gestión y obtención de las conexiones a la base de datos de una manera eficiente. Emplearemos el patrón Singleton para el gestor de conexiones, que en la primera versión tendrá una única conexión, pero que podremos convertir en un conjunto/pool de conexiones.

Estructura de la base de datos

Está formada por una única tabla, Book. La tabla Contido no se usará de momento.

Tabla Book

Columna Tipo de dato Descripción
idBook int Identificador único del ejemplar del libro
isbn varchar(13) Identificador del libro
titulo varchar(255) Título del libro
autor varchar(255) Autor del libro
anho int Año de publicación del libro
disponible boolean Indica si el libro está disponible
portada Blob Portada del libro en formato binario
dataPublicacion Date Fecha de publicación

Script de creación de la tabla

CREATE TABLE PUBLIC."Book" (
    "idBook" INTEGER NOT NULL AUTO_INCREMENT,
    "isbn" CHARACTER VARYING(13) NOT NULL,
    "titulo" CHARACTER VARYING(255) NOT NULL,
    "autor" CHARACTER VARYING(255),
    "anho" INTEGER,
    "disponible" BOOLEAN DEFAULT TRUE,
    "portada" BINARY LARGE OBJECT,
    "dataPublicacion" DATE,
    CONSTRAINT BOOK_PK PRIMARY KEY ("idBook")
);

CREATE UNIQUE INDEX "IdBookPK" ON PUBLIC."Book" ("idBook");
CREATE INDEX "IdxBookISBN" ON PUBLIC."Book" ("isbn");
CREATE INDEX "IdxBookTitle" ON PUBLIC."Book" ("titulo");
CREATE UNIQUE INDEX PRIMARY_KEY_93A ON PUBLIC."Book" ("idBook");


CREATE TABLE PUBLIC.Contido (
	idContido INTEGER NOT NULL AUTO_INCREMENT,
	idBook INTEGER NOT NULL,
	contido CHARACTER LARGE OBJECT,
	CONSTRAINT Contido_PK PRIMARY KEY (idContido)
);
CREATE INDEX FK_ID_BOOK_INDEX_9 ON PUBLIC.Contido (idBook);
CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC.Contido (idContido);

-- PUBLIC.Contido foreign keys

ALTER TABLE PUBLIC.Contido ADD CONSTRAINT FK_ID_BOOK FOREIGN KEY (idBook) REFERENCES PUBLIC.Book(idBook) ON DELETE CASCADE ON UPDATE CASCADE;
DATABASE_TO_UPPER=FALSE

Para permitir nombres en CamelCase en H2 JDBC Driver versión 2, agrega la propiedad DATABASE_TO_UPPER=FALSE en la URL de conexión.

  • Driver: "org.h2.Driver" (no se precisa)
  • URL: "jdbc:h2:rutaBaseDatosSinExtensión;DB_CLOSE_ON_EXIT=TRUE;FILE_LOCK=NO;DATABASE_TO_UPPER=FALSE"

ConnectionManager

Mediante el patrón Singleton, crea una clase ConnectionManager o LibraryConnectionManager con las siguientes características:

  • Atributos:

    • Instancia de la propia clase como atributo privado y final: instance.
    • Atributo privado de tipo Connection que se crea al invocar el método getConnection.
  • Recomendaciones:

    • Puedes hacerlo con Thread-Safe y doble comprobación, pero no es relevante para este caso inicial.
    • Usa constantes privadas para URL, usuario y contraseña, privadas.

Clase Book implementa Serializable.

  • Constructores:
    • Book()
    • Book(String isbn, String title, String author, Short year, Boolean available)
    • Book(Integer idBook, String isbn, String title, String author, Short year, Boolean available, byte[] portada)

Atributos

Atributo Tipo Descripción
idBook Long Identificador único del libro
isbn String Código ISBN del libro
titulo String Título del libro
autor String Autor del libro
anho Short Año de publicación
disponible Boolean Disponibilidad del libro
portada byte[] Portada en formato binario
dataPublicacion LocalDate Data de publicación

Métodos

  • Getters y setters para cada atributo. Los setters devuelven una referencia al propio objeto.

  • setPortada(File f): asigna una portada desde un archivo.

  • setPortada(String f): asigna una portada desde una ruta.

  • Image getImage(): devuelve un objeto de tipo java.awt.Image si la portada no es nula:

    ByteArrayInputStream flujo = new ByteArrayInputStream(portada);
    ImageIO.read(flujo);
  • equals y hashCode: dos libros son iguales si tienen el mismo ISBN. Además, el método hashcode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).

  • toString: Devuelve el título, el autor y el año. Si no está disponible, añade un asterisco.


Clase Contido

package com.javhoz.ad.biblioteca.model;

import java.sql.Connection;
import java.util.Objects;

public class Contido {

    private Long idContido;
    private Long idBook;
    private String contido;

    public Contido() {
    }

    public Contido(Long idBook, String contido) {
        this.idBook = idBook;
        this.contido = contido;
    }

    public Contido(Long idContido, Long idBook) {
        this.idContido = idContido;
        this.idBook = idBook;
    }

    public Contido(Long idContido, Long idBook, String contido) {
        this.idContido = idContido;
        this.idBook = idBook;
        this.contido = contido;
    }

    public Long getIdContido() {
        return idContido;
    }

    public void setIdContido(Long idContido) {
        this.idContido = idContido;
    }


    public Long getIdBook() {
        return idBook;
    }

    public void setIdBook(Long idBook) {
        this.idBook = idBook;
    }

    public String getContido() {
        return contido;
    }

    public void setContido(String contido) {
        this.contido = contido;
    }

    @Override
    public int hashCode() {
        return 97 * 7 + Objects.hashCode(this.idContido);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Contido other)) return false;
        return Objects.equals(this.idContido, other.idContido);
    }

    @Override
    public String toString() {
        return idContido + " (" + idBook + "): " + contido;
    }
}

Interfaz DAO<T>

Esta interface será implantada por todas aquellas clases DAO que trabajen con objetos con imágenes. Los nombres de los métodos son totalmente descriptivos:

T get(long id);
List<T> getAll();
void save(T t);
void update(T t);
void delete(T t);
boolean deleteById(long id);
List<Integer> getAllIds();
void updateLOB(T book, String f);
void updateLOBById(long id, String f);
void deleteAll();

Clase BookDAO implementa DAO<BooK>

Tiene como atributo final un objeto de tipo Connection, con, que recoge como argumento el constructor:

    public BookDAO(Connection con) {
        this.con = con;
    }

Atributos

  • Objeto Connection, pasado al constructor.

Métodos

  1. get(long idBook): devuelve un objeto Book con la información del libro que tiene el identificador pasado como parámetro.
  2. getAll(): devuelve una lista de todos los libros almacenados en la base de datos.
  3. save(Book book): crea un nuevo registro en la tabla Book con la información del libro pasado como parámetro. Importante: además, debe guardar el idBook en el objeto, por lo que es necesario obtener el ID del registro insertado.
    • Debe crearse la sentencia con la opción Statement.RETURN_GENERATED_KEYS.
  4. update(Book book): actualiza la información del registro correspondiente al libro pasado como parámetro.
  5. delete(Book book): elimina el registro correspondiente al libro con el identificador del libro pasado como parámetro.
  6. deleteById(long idBook): elimina el registro correspondiente al libro con el identificador pasado como parámetro.
  7. getAllIds(): devuelve una lista con los ids de todos los libros de la base de datos.
  8. updateLOB(Book b, String f):actualiza el libro en la base de datos con el contenido del archivo recogido como parámetro. Usa setBinaryStream.
    • Ayuda: puedes emplear el método setBinaryStream de PreparedStatement), previamente habiendo leído los bytes.
  9. updateLOBByID(long b, String f): actualiza el libro con el id recogido como por parámetro con el contenido del archivo recogido como parámetro.
  10. deleteAll(): borra todos los libros.

Debes implantar la gestión de sentencias de esta la clase BookDAO por medio de try-with-resources para manejar los cierres de los Statement y los ResultSet de consultas automáticamente.

La conexión no debe cerrarse, pues debe permanecer abierta para futuros usos.

Clase ContidoDAO implementa DAO<Contido>


AppBiblioteca

a) Haz una aplicación que haga uso el ConnectionManager para obtener una conexión y se la pase al constructor de BooKDAO. Crea varios libros y añádelos a la base de datos, incluyendo las portadas de los libros desde la aplicación.

  • Usa ConnectionManager para obtener una conexión y pasarla al constructor de BookDAO.
  • Crea varios libros y añádelos a la base de datos.

Ejemplo de libros:

INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'Cien años de soledad', 'Gabriel García Márquez', 1967, TRUE, NULL);

INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780743273565', 'Harry Potter y la piedra filosofal', 'J.K. Rowling', 1997, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'Cien años de soledad', 'Gabriel García Márquez', 1967, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780743273565', 'Harry Potter y la piedra filosofal', 'J.K. Rowling', 1997, TRUE, NULL);
VALUES ('9780307959474', 'The Sense of an Ending', 'Julian Barnes', 2011, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307386672', 'No Country for Old Men', 'Cormac McCarthy', 2005, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9781400064168', 'The Road', 'Cormac McCarthy', 2006, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780099590088', 'The Noise of Time', 'Julian Barnes', 2016, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780307277672', 'All the Pretty Horses', 'Cormac McCarthy', 1992, TRUE, NULL);
INSERT INTO PUBLIC.Book (isbn, titulo, autor, anho, disponible, portada)
VALUES ('9780099590088', 'Levels of Life', 'Julian Barnes', 2013, TRUE, NULL);

Interface gráfica

Crea una aplicación siguiendo la estructura de MVC con el modelo de datos anterior. Puedes crear una vista o emplear la proporcionada en los apuntes:

Última actualización: 23.09.2025

Proyecto 1ª evaluación


Introducción

El objeto del proyecto es diseñar e implantar un modelo de datos adecuado para una aplicación, tanto para dispositivos móviles como para escritorio.

El resultado final será un modelo de datos compuesto por varias clases, los adaptadores necesarios para trabajar con archivos o la base de datos y una interfaz de usuario que permita interactuar con los datos.

El lenguaje de programación empleado será Kotlin o Java, según se considere más adecuado para el proyecto elegido.

Interoperatividad entre Kotlin y Java

Kotlin es un lenguaje que se puede utilizar de forma interoperable con Java, lo que significa que se pueden mezclar ambos lenguajes en un mismo proyecto. Esto permite a los desarrolladores aprovechar las ventajas de ambos lenguajes y utilizarlos en conjunto en sus aplicaciones.

Incluso en Android Studio, se puede convertir código Java a Kotlin de forma automática o crear el modelo de datos en Java y el resto de la aplicación en Kotlin.

Hazlo como mejor te convenga, pero asegúrate de que el código sea coherente y siga las convenciones de codificación del lenguaje elegido.

El proyecto debe cumplir la siguiente estructura:

  1. Persistencia de datos: se debe implantar la persistencia de datos en la aplicación, utilizando archivos o una base de datos según se considere más apropiado para el proyecto elegido.
    • El formato de los archivos puede ser:
      • Binario.
      • Texto.
      • Archivo de texto en formato JSON (locales o remotos).
    • La base de datos puede ser: Postgres, H2, SQLite o cualquier otra base de datos que consideres adecuada.
    • En el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación móvil, utilizando el patrón de acceso a datos adecuado para la plataforma: ViewModel (para la gestión de la UI), LiveData o StateFlow (para la observación de datos), Room (con SQLite y patrón Repository), Retrofit (para acceso a servicios web), etc.
    • También pueden utilizarse plataformas BaaS (Backend as a Service) como Firebase, Supabase, etc.

Para Firebase, se puede emplear la librería de Firebase para Android, que proporciona una API sencilla para interactuar con la base de datos en tiempo real y el sistema de autenticación de Firebase:

https://developer.android.com/studio/write/firebase https://firebase.google.com/docs/android/setup

  1. Modelo de datos: se debe un modelo de datos que represente la información que se va a manejar en la aplicación. Este modelo debe ser coherente y adecuado para el propósito de la aplicación, siguiendo los estándares de nombrado en Kotlin o Java. Además, debe incluir los adaptadores de Gson en el caso de emplear JSON.

  2. Patrones de acceso a datos: se deben implementar los patrones de acceso a datos necesarios para interactuar con los datos almacenados. Esto puede incluir la creación de clases de acceso a datos, adaptadores, o cualquier otro componente necesario para gestionar la persistencia de datos.

    • Como se ha comentado anteriormentte, en el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación móvil, utilizando el patrón de acceso a datos adecuado para la plataforma: ViewModel (para la gestión de la UI), LiveData o StateFlow (para la observación de datos), Room (con SQLite y patrón Repository), Retrofit (para acceso a servicios web), etc.
    • En el caso de aplicaciones de escritorio, se debe tener en cuenta la gestión de la persistencia de datos en el contexto de una aplicación de escritorio, utilizando el patrón de acceso a datos adecuado para la plataforma: DAO (Data Access Object), JDBC (Java Database Connectivity), etc.
  3. Interfaz de usuario: aunque la parte importante es la parte del modelo y del patrón de arquitectura empleado, se debe implementar una interfaz de usuario que permita interactuar con los datos almacenados.

    • En el caso de aplicaciones móviles, se debe tener en cuenta la gestión de la interfaz de usuario en el contexto de una aplicación móvil, utilizando los componentes de la interfaz de usuario adecuados para la plataforma: Activities, Fragments, Views, etc.
    • En el caso de aplicaciones de escritorio, se debe tener en cuenta la gestión de la interfaz de usuario en el contexto de una aplicación de escritorio, utilizando los componentes de la interfaz de usuario adecuados para la plataforma: JFrame, JPanel, JDialog, etc. No es necesario que siga el patrón MVC, aunque se valorará positivamente si se ha seguido.

Persistencia de datos

Para la persistencia de datos, se debe elegir una de las siguientes opciones:

  1. Persistencia en archivos: se pueden utilizar archivos para almacenar los datos de la aplicación. Los archivos pueden ser de texto, binarios o JSON, y pueden ser almacenados localmente en el dispositivo o de forma remota en un servidor. También pueden emplearse API Rest libres o disponibles en la red, como los ejemplos con los que hemos trabajado en clase. Ejemplos:
    • Almacenar los datos de una lista de tareas pendientes en un archivo de texto o JSON.
    • Almacenar los datos de una lista de contactos en un archivo binario.
    • Consulta de API de películas o libros para obtener información y/o almacenarla localmente.

Existen muchas APIs públicas, por ejemplo:

En el caso de emplear JSON deben crearse serializadores o deserializadores personalizados por medio de Gson.

En Android debe emplearse la clase File para la gestión de archivos y en Java la clase FileReader y FileWriter.

Para acceso a las API Rest, en Android se empleará Retrofit y en Java la clase HttpURLConnection, usando la clases con buffer.

Para Android el API Rest debe estar separada de la vista por medio del patrón MVVM (Model-View-ViewModel) y en Java por medio del patrón DAO (Data Access Object):

https://developer.android.com/topic/libraries/architecture/viewmodel?hl=es-419 https://developer.android.com/codelabs/basic-android-kotlin-training-livedata?hl=es-419#0

En la actualidad se emplea el patrón MVVM en Android, que se basa en la separación de la lógica de negocio de la vista, permitiendo una mayor modularidad y reutilización del código. En Java se emplea el patrón DAO (Data Access Object) para la separación de la lógica de acceso a datos de la lógica de negocio.

Para la retención de datos en Android por medio de MVVM se emplea la clase ViewModel y LiveData para la observación de datos. En Java se emplea la clase DAO para la gestión de la persistencia de datos. Sin embargo, existe un aproximación más actual para

Ejemplo ViewModel con Retrofit

Ejemplo ViewModel con Retrofit en una aplicación Android siguiendo el patrón MVVM (Model-View-ViewModel).

Arquitectura de la aplicación:

├── model
│   └── User.kt
├── network
│   ├── ApiService.kt
│   └── RetrofitClient.kt
├── repository
│   └── UserRepository.kt
└── viewmodel
    └── UserViewModel.kt
├── UserActivity.kt
└── activity_user.xml
     View        

      |

  ViewModel     

      |

   Repository    

      |

   Retrofit      

      |

     API         

1. Dependencias

Primero, asegúrate de tener las siguientes dependencias en tu archivo build.gradle:

dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0") 
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
}

2. Modelo de datos

Define una clase de datos que represente la respuesta de la API:

data class User(
    val id: Int,
    val name: String,
    val email: String
)

3. Configuración Retrofit

Configura Retrofit para realizar las llamadas a la API:

interface ApiService {
    @GET("users")
    suspend fun getUsers(): List<User>
    // Si se le quiere pasar un paámetro a la API
     @GET("users/{id}")
     suspend fun getUser(@Path("id") id: Int): User
}

// Configuración de Retrofit
object RetrofitClient {
    // Esta API es un ejemplo, puedes reemplazarla por la que necesites
    // https://jsonplaceholder.typicode.com/ es un servicio de prueba gratuito
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    val apiService: ApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create()) // Deberías personalizarlo según tus necesidades
            .build()
        retrofit.create(ApiService::class.java)
    }
}

4. Creación el repositorio

El repositorio se encarga de obtener los datos de la API:

class UserRepository {
    private val apiService = RetrofitClient.apiService

    suspend fun getUsers(): List<User> {
        return apiService.getUsers() // Podrían gestionarse errores
    }
}

5. Creación del ViewModel

El ViewModel se comunica con el repositorio y expone los datos a la vista:

class UserViewModel : ViewModel() {
    private val userRepository = UserRepository()
    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> get() = _users

    init {
        fetchUsers()
    }

    private fun fetchUsers() {
        viewModelScope.launch { // Corrutina para llamada a la API en el hilo del viewModel
            try {
                val userList = userRepository.getUsers()
                _users.postValue(userList) // notificar a la vista que los datos han cambiado
            } catch (e: Exception) {
                // Manejar el error
            }
        }
    }
}

6. Conectar el ViewModel con la vista

Finalmente, conecta el ViewModel con tu actividad o fragmento:

class UserActivity : AppCompatActivity() {
    private lateinit var userViewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)

        userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        userViewModel.users.observe(this, Observer { users ->
            // Actualizar la UI con la lista de usuarios
        })
    }
}

Este es un ejemplo básico para empezar. Puedes expandirlo añadiendo manejo de errores más robusto, pruebas unitarias, y otras mejoras según tus necesidades.

Ejemplo de uso de Retrofit en Android con MVVM con corrutinas y LiveData para una API de películas:

/* El servicio de Retrofit se declara con una interfaz que define los métodos de * la API
*  En este caso getPopularMovies() para obtener las películas populares devuelve un objeto de tipo Response<PeliculaResponse>, que permite gestionar la respuesta de la API y errores.
* */
interface PeliculasService {
   @GET("movie/popular")
   suspend fun getPeliculasPopulares(
      @Query("key") claveAPI: String
   ): Response<PeliculaResponse>
}

// El repositorio de películas se encarga de llamar al servicio de Retrofit y gestionar la respuesta (no he creado la clase intermedia RetrofitClient)
// A diferencia del caso anterior en el que se emplea un objeto RetrofitClient, aquí se inyecta el servicio en el constructor del repositorio
class PeliculaRepository(private val servicioPeliculas: PeliculasService) {
   // El identificado "suspend" indica que la función debe ser llamada desde una corrutina
   // y que no bloqueará el hilo principal
   suspend fun getPelicualsPopulares(): Response<PeliculaResponse> {
      return servicioPeliculas.getPeliculasPopulares("tuvalordelaclave")
   }
}

// La respuesta de la API se mapea a un objeto de datos
data class PeliculaResponse(val results: List<Movie>)

// Objeto de datos de película
data class Movie(val title: String, val overview: String, val posterPath: String)

// El ViewModel de películas se encarga de gestionar la lógica de la vista y la llamada a la API
// y de exponer los datos a la vista
class MovieViewModel(private val peliculaRepository: PeliculaRepository) : ViewModel() {
   private val _popularMovies = MutableLiveData<List<Movie>>()
   val popularMovies: LiveData<List<Movie>> = _popularMovies

   fun getPopularMovies() {
      viewModelScope.launch { // Corrutina para llamada a la API en el hilo del viewModel
         val response = peliculaRepository.getPelicualsPopulares()
         if (response.isSuccessful) {
            _popularMovies.value = response.body()?.results
         }
      }
   }
}

La actividad principal debe tener un objeto de la clase MovieViewModel y una lista de objetos de la clase Movie, además de las vistas de la interfaz de usuario:

class MainActivity : AppCompatActivity() {
    private lateinit var movieViewModel: MovieViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

       // Inicializar el ViewModel y pasarle una instancia del repositorio:
       
       val retrofit = Retrofit.Builder()
            .baseUrl("https://api.themoviedb.org/3/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        val peliculasService = retrofit.create(PeliculasService::class.java)
        val peliculaRepository = PeliculaRepository(peliculasService)
       
       movieViewModel = ViewModelProvider(this, MovieViewModelFactory(peliculaRepository)).get(MovieViewModel::class.java)
        movieViewModel.getPopularMovies()

       // Observar los cambios en la lista de películas populares
        movieViewModel.popularMovies.observe(this, Observer { movies ->
            // Actualizar la interfaz de usuario con la lista de películas
            val adapter = MovieAdapter(movies)
            recyclerView.adapter = adapter
           
           
        })
    }
}

Más información sobre Retrofit en Android: Retrofit Más información sobre corrutinas en Android: Corrutinas Más información sobre LiveData en Android: LiveData

StateFlow, FLow vs LiveData

En la actualidad, se recomienda el uso de StateFlow en lugar de LiveData para la observación de datos en Android, ya que StateFlow es más flexible y permite una gestión más eficiente de los datos en la aplicación. Sin embargo, el uso de LiveData sigue siendo válido y es una opción viable para la observación de datos en Android.

https://medium.com/@codzure/livedata-vs-stateflow-the-battle-of-the-observables-730f846be812

StateFlow y LiveData tienen similitudes. Ambas son clases contenedoras de datos observables y siguen un patrón similar cuando se usan en la arquitectura de una app.

Sin embargo, ten en cuenta que StateFlow y LiveData se comportan de manera diferente:

  • StateFlow requiere que se pase un estado inicial al constructor, mientras que LiveData, no.
  • LiveData.observe() cancela automáticamente el registro del consumidor cuando la vista pasa al estado STOPPED, mientras que la recopilación de StateFlow o cualquier otro flujo, no deja de recopilar automáticamente. Para obtener el mismo comportamiento, debes recopilar el flujo desde un bloque Lifecycle.repeatOnLifecycle.
  1. Persistencia en base de datos: se pueden utilizar bases de datos para almacenar los datos de la aplicación. Las bases de datos pueden ser locales o remotas, y pueden ser de distintos tipos (SQLite, Postgres, MySQL, etc.). También se pueden utilizar servicios de bases de datos en la nube. Ejemplos:
    • Almacenar los datos de una lista de tareas pendientes en una base de datos SQLite.
    • Almacenar los datos de una lista de contactos en una base de datos Postgres.
    • Consulta de API de películas o libros para obtener información y almacenarla en una base de datos local o remota.

Ejemplo con Jetpack Compose y StateFlow

Con Jetpack Compose, el enfoque cambia un poco, pero los principios de usar ViewModel y LiveData (o StateFlow) siguen siendo útiles para manejar el estado de la UI de manera reactiva y eficiente.

Jetpack Compose y ViewModel

En Jetpack Compose, puedes seguir usando ViewModel para manejar la lógica de negocio y el estado de la UI. Sin embargo, en lugar de LiveData, es común usar State y StateFlow para una integración más fluida con Compose.

1. Dependencias

Debes tener las siguientes dependencias en tu archivo build.gradle:

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
    implementation "androidx.activity:activity-compose:1.7.0"
    implementation "androidx.compose.ui:ui:1.4.0"
    implementation "androidx.compose.material:material:1.4.0"
    implementation "androidx.compose.ui:ui-tooling-preview:1.4.0"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
}

Con kotlin llevarán paréntesis y con groovy no. Asegúrate de que las versiones sean las más recientes.

2. Creación del ViewModel

Define tu ViewModel usando StateFlow:

class UserViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> get() = _users

    init {
        fetchUsers()
    }

    private fun fetchUsers() {
        viewModelScope.launch {
            try {
                val userList = apiService.getUsers()
                _users.value = userList
            } catch (e: Exception) {
                // Manejar el error
            }
        }
    }
}

3. Creación de la UI con Compose

Usa collectAsState para observar los cambios en StateFlow y actualizar la UI:

@Composable
fun UserScreen(userViewModel: UserViewModel = viewModel()) {
    val users by userViewModel.users.collectAsState()

    LazyColumn {
        items(users) { user ->
            Text(text = user.name)
        }
    }
}

@Composable
fun MyApp() {
    MaterialTheme {
        UserScreen()
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

Ventajas de usar StateFlow con Compose

Puedes usar LiveData con Jetpack Compose, StateFlow ofrece una integración más natural y eficiente con el paradigma declarativo de Compose.

Referencias

Room Persistence Library

MVVM Architecture

Retrofit

UD 3. Object/Relational Mapping (ORM). JPA e Hibernate

Introducción ORM

La persistencia consiste en almacenar los datos de forma permanente.
La persistencia se puede realizar mediante ficheros (planos, XML, JSON,…) o sistemas de base de datos (relacionales, orientados a objetos, JSON, XML, etc.).

En esta unidad vamos a estudiar el almacenamiento en bases de datos relacionales por medio de mapeo objeto-relacional (ORM) y su implementación en Java mediante JPA con Hibernate o EclipseLink, entre otross.

El uso de ficheros se recomienda en pocos casos, como por ejemplo, para almacenar datos de configuración de la aplicación.

Entre las desventajas están:

Persistencia en Sistemas de Bases de Datos

Pueden utilizarse diferentes tipos de bases de datos:

Técnicas de persistencia

  1. JDBC (Java Database Connectivity) Nativa: es una API de Java que permite ejecutar sentencias SQL y procedimientos almacenados en un SGBD.
  2. DAO (Data Access Object): es un patrón de diseño que permite separar la lógica de negocio de la lógica de acceso a datos.
    Cada clase del modelo de datos tiene su clase DAO asociada con método para realizar las operaciones CRUD (Create, Read, Update, Delete).
  3. Frameworks de persistencia/ORM (Object/Relational Mapping): son librerías que permiten realizar la persistencia de datos de forma transparente.
    1. JPA (Java Persistence API): es una especificación de una API de Java que permite mapear objetos Java a tablas de una base de datos relacional.
    2. Implementaciones de JPA o nativas: existen varias implementaciones de la especificación JPA, pero la más popular es Hibernate, un framework de persistencia que implementa la especificación JPA. Otras:

Subsecciones de UD 3. Object/Relational Mapping (ORM). JPA e Hibernate

03.01. Jakarta Persistence (JPA).

Mapeo Objeto-Relacional (ORM)

Mapeo Objeto-Relacional (ORM) es el proceso de convertir objetos Java en tablas de bases de datos. Esto permite interactuar con una base de datos relacional sin necesidad de utilizar SQL.

Jakarta/Java Persistence API (JPA) es una especificación que define cómo persistir datos en aplicaciones Java. El enfoque principal de JPA es la capa de ORM.

Hibernate es uno de los frameworks de ORM más populares en uso hoy en día y una implementación estándar de la especificación JPA, con algunas características adicionales específicas de Hibernate. Su primera versión se lanzó hace casi veinte años y aún cuenta con un excelente soporte de la comunidad y lanzamientos regulares.

En esta unidad nos centraremos en Jakarta Persistence API (JPA) con Hibernate, aunque también veremos alguna otra implementación de referencia de JPA, como EclipseLink o DataNucleus.
SpringBoot Data JPA utiliza Hibernate como implementación de JPA, pero, en cuanto a rendimiento, no es la mejor opción.

Arquitectura ORM Arquitectura ORM

Referencias

Subsecciones de 03.01. Jakarta Persistence (JPA).

01. Jakarta Persistence (JPA).

1. Jakarta Persistence

Desde los primeros días de la plataforma Java, han existido interfaces de programación para proporcionar pasarelas hacia la base de datos y para abstraer las necesidades de persistencia específicas del dominio de las aplicaciones empresariales.

Jakarta Persistence define un estándar para la gestión de persistencia y el mapeo objeto/relacional en entornos Java basado en POJO (Plain Old Java Object) para la persistencia en Java.

Jakarta Persistence es sólo una especificación que no puede realizar la persistencia por sí misma. Por supuesto, Jakarta Persistence requiere una base de datos para persistir.

El API Jakarta para la gestión de persistencia y el mapeo objeto/relacional puede emplearse en Jakarta EE o Java SE.

En la actualidad, existen varias soluciones de persistencia en Java. Nos centraremos en la especificación JPA y soluciones propietarias como Hibernate, EclipseLink, DataNucleus, etc.

La API de Persistencia de Jakarta consta de cuatro áreas:

  • La Jakarta Persistence API (JPA)
  • La API de Criterios de Persistencia de Jakarta (Jakarta Persistence Criteria API)
  • El Lenguaje de Consulta de Persistencia de Jakarta (JPQL,Jakarta Persistence Query Language)
  • Metadatos de mapeo objeto-relacional

1.1. Historia

La API de Java Persistence (JPA) es una especificación de Java EE que describe cómo administrar datos relacionales en aplicaciones empresariales de Java. La API de JPA se basa en la especificación de Java Data Objects (JDO), especificación de Java EE que describe cómo administrar datos en aplicaciones empresariales de Java.

  • JPA 1.0: la fecha de lanzamiento final de la especificación JPA 1.0 fue el 11 de mayo de 2006 como parte del Java Community Process JSR 220.
  • JPA 2.0 se lanzó el 10 de diciembre de 2009 (la plataforma Java EE 6 requiere JPA 2.0).
  • JPA 2.1 se lanzó el 22 de abril de 2013 (la plataforma Java EE 7 requiere JPA 2.1).
  • JPA 2.2 se lanzó en el verano de 2017.
  • JPA 2.3 se lanzó en el verano de 2019.
  • JPA 3.0 se lanzó en el verano de 2020. Fue renombrada a Jakarta Persistence 3.0 (requiere Java 8). Así, todos los paquetes se renombraron de javax.persistence a jakarta.persistence. Implementaciones:
    • Hibernate (desde versión 5.5)
    • EclipseLink (desde versión 3.0)
    • DataNucleus (desde versión 6.0)
  • JPA 3.1 se lanzó en la primavera de 2022 como parte de Jakarta EE 10 (requiere Java 11). Implementaciones:
    • Hibernate (desde versión 6.0)
    • EclipseLink (desde versión 4.0)
    • DataNucleus (desde versión 6.0)
  • JPA 3.2 se lanzó el 30 de abril de 2024. Implementaciones:

La novedades de Jakarta Persistence 3.2 se pueden encontrar en este enlace: https://jakarta.ee/specifications/persistence/3.2/

1.2. Las versiones de Jakarta Persistence

La especificación Jakarta Persistence 3.1 es la primera versión con nuevas características y mejoras después de que la especificación se trasladara a la Eclipse Foundation (jakarta.persistence).

Java Persistence API 2.0 (2009)

La segunda versión Java Persistence 2.0 en 2009. Incluyó varias características que no estaban presentes en la primera versión:

  • Capacidades de mapeo adicionales.
  • Formas flexibles de determinar la forma en que el proveedor accedía al estado de la entidad.
  • Extensiones al Lenguaje de Consulta de Persistencia de Java (JPQL).
  • Nueva API de Criterios de Java, una forma programática de crear consultas dinámicas.

Java Persistence 2.1 (2013)

Java Persistence 2.1 en 2013 agregó algunas características:

  • Soporte para generación de esquemas.
  • Métodos de conversión de tipos.
  • Creación de gráficos de entidades y pasarlos a consultas, lo que se conoce comúnmente como restricciones de grupo de recuperación en el conjunto de objetos devueltos.
  • Contextos de persistencia no sincronizados para operaciones conversacionales mejoradas.
  • Soporte para procedimientos almacenados.
  • Inyección en clases de escuchadores de entidades.
  • Mejoras en el lenguaje de consulta de Java Persistence, la API de criterios y en el mapeo de consultas nativas.

Java Persistence 2.2 (2017)

Java Persistence 2.2 fue publicada por Oracle en junio de 2017:

  • Métodos para recuperar los resultados de las consultas (Query) y consultas tipadas (TypedQuery) como flujos (streams).
  • Soporte para tipos básicos de Fecha y Hora de Java 8: java.time.LocalDate, java.time.LocalTime, java.time.LocalDateTime, java.time.OffsetTime y java.time.OffsetDateTime.
  • Permitir que los convertidores de atributos admitan la inyección de CDI.
  • Actualización del mecanismo de descubrimiento del proveedor de persistencia.
  • Permitir que todas las anotaciones de Java Persistence se utilicen en metaanotaciones.

Jakarta Persistence 2.2 se puede encontrar aquí.

Jakarta Persistence 3.0 (2020)

Jakarta Persistence 3.0, lanzada en 2020, fue el cambio al espacio de nombres del paquete jakarta. Trasladó las API existentes del paquete javax.persistence al paquete jakarta.persistence. Todas las propiedades que contienen javax como parte del nombre se renombran de manera que javax se reemplace con jakarta.

Actualización de los espacios de nombres del esquema para un archivo de configuración de unidad de persistencia y un archivo XML de mapeo objeto-relacional.

Jakarta Persistence 3.1 (2021)

El lanzamiento de Jakarta Persistence 3.1 fue publicado por la Eclipse Foundation en diciembre de 2021.

En general, los cambios en Jakarta Persistence 3.1 incluyeron:

  • Estandarización de la función EXTRACT en el Lenguaje de Consulta de Persistencia de Jakarta.
  • Estandarización de la Generación de UUID para claves primarias.
  • Definición del nombre del módulo jakarta.persistence para la API de Persistencia de Jakarta para el Sistema de Módulos de Plataforma Java.
  • Permitir que las interfaces EntityManagerFactory y EntityManager extiendan la interfaz java.lang.AutoCloseable.
  • Actualizaciones editoriales y aclaraciones en la especificación.

Para obtener una lista completa de cambios, consulta la sección de Historial de Revisiones del Documento de Especificaciones disponible en este enlace.

1.3. Referencias

Dependencias Maven, Gradle, Ivy, SBT para Jakarta Persistence 3.1:

<dependency>
  <groupId>jakarta.persistence</groupId>
  <artifactId>jakarta.persistence-api</artifactId>
  <version>3.1.0</version>
</dependency>

Versiones:

2. Jakarta Persistence (JPA)

Jakarta Persistence, anteriormente conocida como Java Persistence API, es una especificación de interfaz de programación de aplicaciones de Jakarta EE que describe la gestión de datos relacionales en aplicaciones empresariales de Java.

Como se ha comentado, JPA abarca cuatro áreas:

  1. La API en sí, definida en el paquete jakarta.persistence (javax.persistence para Jakarta EE 8 y versiones anteriores).
  2. La API de Criterios de Persistencia de Jakarta (Jakarta Persistence Criteria API)
  3. El Lenguaje de Consulta de Jakarta Persistence (JPQL; anteriormente Lenguaje de Consulta de Java Persistence) que permite realizar consultas a una base de datos relacional obteniendo colecciones de objetos.
  4. Metadatos objeto/relacional: la configuración puede hacerse con anotaciones (@Id, @Entity,…) o mediante ficheros XML.

Características:

  • JPA es una especificación (no implementación) que facilita el mapeo objeto-relacional para gestionar datos relacionales en aplicaciones Java.

  • No se puede utilizar JPA directamente. Deben emplearse implementaciones ORM como Hibernate, EclipseLink, MyBatis (antes IBatis), DataNucleus,.. que emplean la especificación de JPA.

  • La última versión con implementaciones estables es la 3.1, que se lanzó en la primavera de 2022 como parte de Jakarta EE 10 (requiere Java SE 11 o superior).
    Algunas de las implementaciones compatibles con esta especificación son:

  • La mayoría de las herramientas ORM como Hibernate, MyBatis (antes IBatis) o EclipseLink, que es la implementación de referencia, implementan este estándar.

  • JPA proporciona soporte para trabajar directamente con objetos en lugar de utilizar declaraciones SQL.

  • Dispone de un fichero de configuración denominado persistence.xml.

Consejo

JPA define un proceso de inicio diferente, junto con un formato estándar de archivo de configuración llamado persistence.xml. En entornos de Java™ SE, se requiere que el proveedor de persistencia (Hibernate, EclipseLink,…) localice cada archivo de configuración de JPA en el classpath en la ruta META-INF/persistence.xml.

Los XML Schemas de Jakarta Persistence se pueden encontrar en https://jakarta.ee/xml/ns/persistence/.

2.2. Implementaciones JPA

La API de Jakarta Persistence proporciona métodos para administrar la persistencia de objetos a un almacén de datos relacional. La implementación de referencia para JPA es EclipseLink, pero existen otras que cubren las necesidades de los desarrolladores, como:

  • Hibernate: es una solución de Mapeo Objeto/Relacional (ORM) para programas escritos en Java y otros lenguajes que admiten la JVM.
  • EclipseLink: es una solución de persistencia de objetos para Java.
  • Spring Data JPA: es una biblioteca de Spring que simplifica el acceso a los sistemas de almacenamiento de datos relacionales. Se basa en la tecnología de acceso a datos de Spring y utiliza las características de JPA para simplificar el acceso a los sistemas de almacenamiento de datos relacionales.
  • Apache OpenJPA: es una implementación de JPA que puede utilizarse como un almacén de datos independiente o como una extensión de Apache Geronimo.
  • Oracle TopLink: es una solución de persistencia de objetos para Java. Desarrollada por Oracle con licencia dual, tanto comercial como de código abierto que derivó en Eclipse Link. Podría decirse que ya está descontinuado.
  • DataNucleus: es una solución de persistencia de objetos para Java, OSGi y la plataforma de Google App Engine. Es una implementación de JDO y JPA.

Por supuesto, también es posible desarrollar una implementación propia de JPA.

Nos centraremos en Hibernate, que es una de las implementaciones más populares (y mejor) de JPA, pero podremos utilizar cualquiera de las otras implementaciones.

Dependencias Maven

Se precisa la especificación de Jakarta Persistence API y la implementación. Para Hibernate, la dependencia Maven sería:

<!-- Se precisa la dependencia de Hibernate y la de la API de Jakarta Persistence -->
<dependency>
  <groupId>jakarta.persistence</groupId>
  <artifactId>jakarta.persistence-api</artifactId>
  <version>3.1.0</version>
</dependency><!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core -->
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.6.4.Final</version>
</dependency>
Ejercicio 01.01. Creación de un proyecto con JPA

Para crear un proyecto con JPA y Hibernate, se puede utilizar el asistente de creación de proyectos de Eclipse o IntelliJ IDEA, sin embargo con la versión Community de IntelliJ IDEA no se puede crear un proyecto con JPA a través del asistente. Crea un proyecto Java Maven y añade las dependencias de Hibernate y la API de Jakarta Persistence.

2.3 Fichero de configuración persistence.xml

Los artefactos (y elementos de configuración) de la unidad de persistencia se suelen empaquetar en un “archivo de persistencia”. Un archivo con formato JAR que contiene el archivo persistence.xml en el directorio META-INF y los archivos de clase de entidad (clases de persistencia).

Para desplegar la aplicación se precisa situar el archivo de persistencia, las clases de aplicación que utilizan las entidades y los archivos JAR del proveedor de persistencia (JDBC) en el classpath cuando se ejecuta el programa.

La configuración que describe la unidad de persistencia se define en un archivo XML llamado META-INF/persistence.xml. Cada unidad de persistencia tiene un nombre, por lo que cuando una aplicación de referencia desea especificar la configuración para una entidad, solo necesita hacer referencia al nombre de la unidad de persistencia que define esa configuración. Un solo archivo persistence.xml puede contener una o más configuraciones de unidades de persistencia con nombres, pero cada unidad de persistencia es independiente y distinta de las demás

Los ==únicos que necesitamos especificar para este ejemplo son name, transaction-type, class y properties=0.

<persistence>
  <persistence-unit name="ServicioEmpleado"
                    transaction-type="RESOURCE_LOCAL">
    <class>com.javhoz.ad.modelo.Empleado</class>
    <properties>
      <property name="jakarta.persistence.jdbc.driver"
                value="org.apache.derby.jdbc.ClientDriver"/>
      <property name="jakarta.persistence.jdbc.url"
                value="jdbc:derby://localhost:1527/EmpServDB;
create=true"/>
      <property name="jakarta.persistence.jdbc.user"
          value="APP"/>
      <property name="jakarta.persistence.jdbc.password"
          value="APP"/>
    </properties>
  </persistence-unit>
</persistence>

Ejemplo de configuración con Hibernate y H2 en memoria habría que indicar el proveedor de persistencia org.hibernate.jpa.HibernatePersistenceProvider, Hibernate, y la base de datos que se va a utilizar, que en el ejemplo H2 en memoria:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="com.javhoz.ad.jpa.example" transaction-type="RESOURCE_LOCAL">
        <description>Ejemplo de unidad de persistencia con Hibernate y H2 en memoria</description>
        
        <!-- 1. El proveedor de persistencia ES OPCIONAL, pero se recomienda -->
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <!-- Hibernate -->
        <!-- para EclipseLink sería: <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> -->
        
        <!-- 2. Escanea las clases y las detecta automáticamente. En caso contrario 
        habría que indicarlo con "class" -->
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <!-- propiedades de JPA: -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <!-- Si usamos la configuración de Hibernate: -->
            <!-- <property name="hibernate.connection.driver_class" value="org.h2.Driver"/> -->
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/>
            <!-- Si usamos la configuración de Hibernate: -->
            <!-- <property name="hibernate.connection.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"/> -->
            <property name="jakarta.persistence.jdbc.user" value="sa"/>
            <!-- Si usamos la configuración de Hibernate: -->
            <!-- <property name="hibernate.connection.username" value="sa"/> -->
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/> <!-- create, drop-and-create, none, drop -->
            <property name="jakarta.persistence.lock.timeout" value="100"/>
            <property name="jakarta.persistence.query.timeout" value="100"/>
            <property name="jakarta.persistence.validation.mode" value="NONE"/>
           
            <!-- propiedades de Específicas de Hibernate: -->
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.connection.pool_size" value="50"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
<!--            <property name="hibernate.hbm2ddl.auto" value="create-drop"/> &lt;!&ndash; create-drop, update, create, validate &ndash;&gt;-->
            <property name="hibernate.max_fetch_depth" value="5"/>
            <property name="hibernate.cache.region_prefix" value="hibernate.test"/>
            <property name="hibernate.cache.region.factory_class"
                      value="org.hibernate.testing.cache.CachingRegionFactory"/>
            <!--NOTE: hibernate.jdbc.batch_versioned_data debe ponerse como "false" en Oracle -->
            <property name="hibernate.jdbc.batch_versioned_data" value="true"/>
            
            <property name="hibernate.service.allow_crawling" value="false"/>
            <property name="hibernate.session.events.log" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Para Hibernate con MySQL con JPA 3.1, por ejemplo, sería:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="jpa-hibernate-mysql">
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" />
            <property name="jakarta.persistence.jdbc.url"    value="jdbc:mysql://localhost:3306/ejemploDBHibernate" />
            <property name="jakarta.persistence.jdbc.user"   value="root" />
            <property name="jakarta.persistence.jdbc.password" value="" />
            <property name="jakarta.persistence.schema-generation.database.action" value="create" />
            
            <property name="hibernate.dialect"    value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.show_sql"   value="true" />
            <property name="hibernate.format_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

Ejemplos de Dialectos de Hibernate 6, que se pueden utilizar en la propiedad hibernate.dialect, y no son más que clases que implementan la interfaz Dialect:

AbstractHANADialect, AbstractTransactSQLDialect, CockroachDialect, DB2Dialect, DerbyDialect, DialectDelegateWrapper, HSQLDialect, MySQLDialect, OracleDialect, PostgreSQLDialect, SpannerDialect.

  • Para cambiar de implementación de EclipseLink a Hibernate en la aplicación Java, sólo se precisa cambiar el fichero de configuración de la aplicación, denominado persistence.xml. Sin embargo, Hibernate y EclipseLink tienen algunas características específicas que no están incluidas en la especificación JPA. Por lo tanto, si utilizas estas características específicas, no podrás cambiar de implementación y deberás utilizar la implementación específica, con los respectivos archivos de configuración:

    • hibernate.cfg.xml para Hibernate y
    • eclipselink.xml para EclipseLink.

Si se utiliza persistence.xml, la especificación sigue siendo la misma. Esa es la ventaja de utilizar JPA.

Ejercicio 01.02. Creación de un archivo de configuración de persistencia

Crea un directorio META-INF en el directorio src/main/resources y añade un archivo persistence.xml con la configuración de la unidad de persistencia con el nombre com.sanclemente.ad.jpa.exemplo.

El fichero de configuración persistence.xml debe apuntar a una base de datos H2 en memoria. Además, debes añadir los Drivers de H2 para que la aplicación pueda conectarse a la base de datos:

<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.3.232</version>
</dependency>

Ten en cuenta que precisas crear la base de datos en memoria H2 y añadir las tablas necesarias, por lo que el parámetro jakarta.persistence.schema-generation.database.action debe ser “create”.

2.3. Entidades/Entity

Entidades, valores y tablas Entidades, valores y tablas

Una entidad es una clase que representa un objeto persistente almacenado en una base de datos relacional.

Para que una clase sea una Entidad debe cumplir:

  • Debe ser una clase POJO (Plain Old Java Object): POJO es un objeto Java que no está sujeto a ninguna restricción de las impuestas por la Especificación del lenguaje Java (sin herencias, implementaciones, dependencias de bibliotecas, etc. ). Sólo puede tener:
    • Atributos.
    • Constructores.
    • getters y setters (además de métodos de Object…)
  • Debe tener un constructor por defecto NO privado.
  • Puede tener constructores adicionales y declararse como abstracta.
  • No debe ser una clase interna (aunque puede ser una clase anidada estática).
  • No puede ser final.
  • Suelen implantar java.io.Serializable (aunque no es obligatorio en entornos SE).
  • Para convertirla en una entidad debe tener la anotación @Entity, declarada en jakarta.persistence.Entity.
  • Debe tener un identificador (ID) que se puede definir con la anotación @Id (declarada en jakarta.persistence.Id). El identificador puede ser de cualquier tipo, aunque lo más habitual es que sea un tipo primitivo o un objeto de tipo java.lang.Long o java.lang.Integer. Dicho identificador debe ser único para cada entidad y está asociado a la clave primaria de la tabla de la base de datos.

Ejemplo de declaración de una Entity/clase Persona:

import jakarta.persistence.*;

import java.util.UUID;

@Entity
public class Persona {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO, SEQUENCE, TABLE, IDENTITY, UUID
    private Long id;

//    @Id
//    @GeneratedValue(strategy = GenerationType.UUID)
//    private UUID id;

    private String nome;


    public Persona() {
    }

    public Persona(String nome) {
        this.nome = nome;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    @Override
    public String toString() {
        return "Persona{" +
                "id=" + id +
                ", nome='" + nome + '\'' +
                '}';
    }
}

Anotaciones para la clase (lo veremos más adelante al detalle):

  • @Entity: indica que la clase es una entidad. Elementos: -name (String): el nombre de la entidad empleado en las consultas. Por defecto, el nombre de la clase (sin paquete). Por ejemplo: @Entity(name = "Persoa").

  • @Table: especifica el nombre de la tabla de la base de datos. Si no se indica, el nombre de la tabla es el nombre de la clase. Por ejemplo: @Table(name = “persona”). Elementos

    • name (String): el nombre de la tabla de la base de datos.
    • catalog (String): el nombre del catálogo de la base de datos.
    • schema (String): el nombre del esquema de la base de datos.
    • uniqueConstraints (de tipo UniqueConstraint[]): las restricciones de unicidad de la tabla de la base de datos.
    • indexes (Index[]): los índices de la tabla de la base de datos, para generación.

Anotaciones para los atributos: por defecto se mapean todos los atributos de la clase, pero se pueden excluir con la anotación @Transient.

  • @Id: Indica que el atributo es la clave primaria de la entidad.
  • @GeneratedValue: Indica que el valor del atributo es generado automáticamente por el sistema de persistencia. Posibles valores:
    • AUTO: El sistema de persistencia elige la estrategia de generación de claves primarias.
    • IDENTITY: El sistema de persistencia utiliza una columna de tipo autoincremental.
    • SEQUENCE: El sistema de persistencia utiliza una secuencia de base de datos.
    • TABLE: El sistema de persistencia utiliza una tabla adicional de base de datos.
    • UUID: El sistema de persistencia utiliza un UUID (JPA 3.1), identificador único universal, que es un número de 128 bits.
  • @Transient: Indica que el atributo no es persistente, es decir, no se almacena en la base de datos.
  • @Column: Indica que el atributo es una columna de la tabla de la base de datos. Permite definir el nombre de la columna, el tipo de datos, etc. Por ejemplo: @Column(name = “nombre”, nullable = false, length = 50):
    • name: Indica el nombre de la columna de la base de datos.
    • nullable: Indica si el atributo puede tener valores nulos (true) o no (false).
    • length: Indica la longitud máxima del atributo.
    • unique: Indica si el atributo debe ser único (true) o no (false).
    • insertable: Indica si el atributo se debe insertar en la base de datos (true) o no (false).
    • updatable: Indica si el atributo se debe actualizar en la base de datos (true) o no (false).
    • precision: Indica el número de dígitos de precisión de un atributo de tipo numérico.
    • scale: Indica el número de dígitos decimales de un atributo de tipo numérico.

Se mapean automáticamente los atributos de la clase con los campos de la tabla de la base de datos con el mismo nombre. Por ejemplo, el atributo nome se mapea con el campo nome de la tabla de la base de datos. Los tipos admitidos son los siguientes:

  • Tipos primitivos: int, long, float, double, boolean, char, byte, short.
  • Tipos envolventes de los tipos primitivos: Integer, Long, Float, Double, Boolean, Character, Byte, Short.
  • String.
  • java.util.Date.
  • java.util.Calendar.
  • java.sql.Date.
  • java.sql.Time.
  • java.sql.Timestamp.
  • java.math.BigDecimal.
  • java.math.BigInteger.
  • byte[].
  • java.util.UUID
  • java.time.LocalDate
  • java.time.LocalTime
  • java.time.LocalDateTime
  • java.time.OffsetTime

Los atributos que no se pueden mapear automáticamente con los campos de la tabla de la base de datos se deben excluir con la anotación @Transient y se deben mapear manualmente con la anotación @Column.

Datos temporales:

  • @Temporal: Indica que el atributo es un dato temporal (java.util.Date, java.util.Calendar, java.sql.Date, java.sql.Time, java.sql.Timestamp). Por ejemplo: @Temporal(TemporalType.DATE). Así como @Temporal(TemporalType.TIME) y @Temporal(TemporalType.TIMESTAMP).
Ejercicio 01.03. Creación de una entidad

Crea una entidad Estudiante con idEstudiante (Long), nombre, apellidos, fechaDeNacimiento y dirección. Añade los atributos necesarios y las anotaciones para que sea una entidad. La clave primaria será idEstudiante de tipo autoincremental.

Ejercicio 01.04. Creación de una entidad

Crea una clase AppEstudiante que se conecte a la base de datos y añada un estudiante a la tabla de la base de datos.

Aunque lo veremos más adelante, lo que precisamos es crear un gestor de entidades e invocar al método persist para añadir un estudiante a la base de datos:

public class AppEstudiante {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("com.sanclemente.ad.jpa.exemplo");
        EntityManager em = emf.createEntityManager();

        Estudiante estudiante = new Estudiante("Juan", "Pérez", LocalDate.of(2000, 1, 1), "Calle Mayor, 1");

        em.getTransaction().begin();
        em.persist(estudiante);
        em.getTransaction().commit();
        
        // IMprime el estudiante para ver si se ha añadido correctamente y tiene un id

        em.close();
        emf.close();
    }
}

Para recuperarlo precisamos invocar al método find del gestor de entidades:

Estudiante estudiante = em.find(Estudiante.class, 1L); // Recupera el estudiante con id 1

2.4. Relaciones

Una relación es una “relación” entre dos entidades.
Puede ser unidireccional o bidireccional.

  • Una relación unidireccional tiene una entidad de origen y una entidad de destino.
  • Una relación bidireccional tiene una entidad de origen y una entidad de destino, pero también tiene una entidad de destino y una entidad de origen. Una relación bidireccional tiene dos lados: el lado propietario y el lado inverso. El lado propietario de una relación bidireccional determina qué entidad de la relación se actualizará en la base de datos cuando se actualice la relación en el código. El lado inverso de una relación bidireccional se actualiza automáticamente siempre que se actualice el lado propietario.

2.5. Tipos de relaciones

Las relaciones entre entidades pueden ser de los siguientes tipos:

  • Uno a uno: una entidad de origen se asocia con una entidad de destino. Una entidad de destino también se asocia con una entidad de origen. Por ejemplo, una persona tiene un pasaporte y un pasaporte pertenece a una persona.
  • Uno a muchos: una entidad de origen se asocia con una colección de entidades de destino. Una entidad de destino se asocia con una entidad de origen. Por ejemplo, una persona tiene varias direcciones y cada dirección pertenece a una persona.
  • Muchos a uno: una entidad de origen se asocia con una entidad de destino. Una entidad de destino se asocia con una colección de entidades de origen. Por ejemplo, una dirección tiene una persona y una persona pertenece a varias direcciones.
  • Muchos a muchos: una entidad de origen se asocia con una colección de entidades de destino. Una entidad de destino se asocia con una colección de entidades de origen. Por ejemplo, una persona tiene varios teléfonos y un teléfono pertenece a varias personas.
Última actualización: 23.09.2025

02. JPA vs Hibernate.

JPA

  • JPA significa Java Persistence API (Interfaz de Programación de Aplicaciones).

  • Fue lanzado inicialmente el 11 de mayo de 2006.

  • Es una especificación de Java que proporciona funcionalidad y estándares para herramientas de Mapeo Objeto-Relacional (ORM).

  • Se utiliza para examinar, controlar y persistir datos entre objetos Java y bases de datos relacionales.

  • Se considera como una técnica estándar para el Mapeo Objeto-Relacional.

  • Se le considera como un enlace entre un modelo orientado a objetos y un sistema de base de datos relacional.

  • Como es una especificación de Java, JPA no realiza ninguna funcionalidad por sí misma. Por lo tanto, necesita una implementación. De este modo, para la persistencia de datos, herramientas ORM como Hibernate implementan las especificaciones de JPA. Para la persistencia de datos, el paquete jakarta.persistence (antes javax.persistence) contiene las clases e interfaces de JPA.

  • JPA es solo una especificación, no es una implementación.

  • Es un conjunto de reglas y pautas para establecer interfaces para la implementación del mapeo objeto-relacional.

  • Necesita algunas clases e interfaces.

  • Admite un mapeo objeto-relacional simple, limpio y asimilado.

  • Admite polimorfismo e herencia.

  • Pueden incluirse consultas dinámicas y con nombre en JPA.

Hibernate

  • Es un Framework de Java, de código abierto, ligero y una herramienta de Mapeo Objeto-Relacional (ORM) para el lenguaje Java que simplifica la construcción de aplicaciones Java para interactuar con la base de datos.
  • Se utiliza para guardar objetos Java en el sistema de base de datos relacional.
  • Hibernate es una implementación de que sigue el estándar de JPA.
  • Ayuda a mapear los tipos de datos Java a los tipos de datos SQL.
  • Contribuye a JPA.

Nota: El framework de Hibernate ORM fue inicialmente diseñado por Red Hat. Se lanzó el 23 de mayo de 2007. Es compatible con JVM multiplataforma y está escrito en Java.

La característica principal de Hibernate es mapear las clases Java a tablas de base de datos.

JPA es una especificación. Proporciona funcionalidad y prototipo comunes para las herramientas ORM. Todas las herramientas ORM (como Hibernate) siguen los estándares comunes, ejecutando la misma especificación. Por lo tanto, si necesitamos cambiar nuestra aplicación de una herramienta ORM a otra, podemos hacerlo fácilmente.

Como sabemos, JPA es solo una especificación, lo que significa que no hay implementación. Podemos anotar clases en la medida que queramos con anotaciones de JPA, aunque, nada sucederá sin una implementación. Supongamos que JPA son las pautas que deben seguirse, sin embargo, Hibernate es un código de implementación de JPA que une la API según lo descrito por la especificación de JPA y proporciona la funcionalidad anónima.

Diferencias entre JPA e Hibernate:

JPA Hibernate
Está descrito en el paquete jakarta.persistence (+3.0) javax.persistence (2.3 o inferior). Está descrito en el paquete org.hibernate.
Describe el manejo de datos relacionales en aplicaciones Java. Hibernate es una herramienta de Mapeo Objeto-Relacional (ORM) que se utiliza para guardar objetos Java en un sistema de base de datos relacional.
No es una implementación, es solo una especificación de Java. Hibernate es una implementación de JPA. Por lo tanto, sigue el estándar común proporcionado por JPA.
Es una API estándar que permite realizar operaciones en la base de datos. Se utiliza para mapear tipos de datos Java con tipos de datos SQL y tablas de base de datos.
Utiliza Java Persistence Query Language (JPQL) como lenguaje de consulta orientado a objetos. Utiliza Hibernate Query Language (HQL) como lenguaje de consulta orientado a objetos.
Utiliza la interfaz EntityManagerFactory para interactuar con la fábrica del administrador de entidades para la unidad de persistencia. Utiliza la interfaz SessionFactory para crear instancias de sesión.
Utiliza la interfaz EntityManager para realizar acciones de crear, leer y eliminar para instancias de clases de entidad mapeadas. Utiliza la interfaz Session para realizar acciones de crear, leer y eliminar para instancias de clases de entidad mapeadas.
Actúa como una interfaz de tiempo de ejecución entre una aplicación Java y Hibernate. Actúa como una interfaz de tiempo de ejecución entre una aplicación Java y Hibernate.

La principal diferencia entre Hibernate y JPA es que Hibernate es un framework mientras que JPA son especificaciones de API. Hibernate es la implementación de todas las pautas de JPA.

Última actualización: 23.09.2025

03. Ejercicio básico de JPA.

1. Añadir dependencias

Hibernate se divide en varios módulos/artefactos bajo el grupo org.hibernate.orm. El artefacto principal se llama hibernate-core.

    <dependencies>
    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter 
    Pruebas unitarias -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.1</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Dependencias para conexiones a bases de datos.
        Sólo necesitamos la que vayamos a emplear. -->
    <!-- https://mvnrepository.com/artifact/com.h2database/h2 
        Ojo con la versión. Si empleamos la versión 2.2.224 tendremos 
        que getionar la versión de Driver en DBeaver -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>2.3.232</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.4</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>9.1.0</version>
    </dependency>
    
    <!--  JPA -->
    <dependency>
        <groupId>jakarta.persistence</groupId>
        <artifactId>jakarta.persistence-api</artifactId>
        <version>3.1.0</version>
    </dependency>
    
    <!-- Implementaciones JPA. Usaremos una u otra.-->
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core -->
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.6.4.Final</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/org.eclipse.persistence/org.eclipse.persistence.jpa -->
<!--    <dependency>
        <groupId>org.eclipse.persistence</groupId>
        <artifactId>org.eclipse.persistence.jpa</artifactId>
        <version>4.0.5</version>
    </dependency>-->
</dependencies>

2. Creación del archivo de configuración persistence.xml

JPA define un proceso de arranque diferente al nativo de Hibernate, junto con un formato de archivo de configuración estándar denominado persistence.xml. En entornos Java™ SE, se requiere que el proveedor de persistencia (Hibernate, EclipseLink, etc.) ubique cada archivo de configuración JPA en la ruta de clases en la ruta META-INF/persistence.xml.

Añadidlo al directorio maven: src/main/resources/META-INF/persistence.xml.

2.1. Para Hibernate

Por ejemplo, para hibernate y h2:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

    <!--nombre único de la unidad de persistencia-->
    <persistence-unit name="ejemplopersistenciaJPA">   
        <description>
            Ejemplo de unidad de persistencia para Jakarta Persistence
        </description>
                <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<!--        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>-->
        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <!-- Clases que se van a persistir -->
<!--        <class>com.javhoz.ad.orm.Usuario</class>     -->

        <!-- Propiedades de la unidad de persistencia -->
        <properties>    
            <!-- Configuración de conexión a base de datos. H2 en memoria. -->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:E:/ruta/baseDatos;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO;DB_CLOSE_DELAY=-1" />
<!--            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1" />-->
            <property name="jakarta.persistence.jdbc.user" value="" />
            <property name="jakarta.persistence.jdbc.password" value="" />
            <!-- crete: automáticamente, genera el esquema de la base de datos.
                none: no hace nada (la base de datos debe existir)
                create: crea las tablas (si no existen)
                drop-and-create: borra las tablas y las vuelve a crear.
                drop: borra las tablas cuando se cierra la factoría de persistencia, pero no las vuelve a crear. 
             -->
            <property name="jakarta.persistence.schema-generation.database.action" value="create" /> <!-- none, create, drop-and-create, drop  -->

            <!-- Muestra por pantalla las sentencias SQL -->
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="hibernate.highlight_sql" value="true" />
            <property name="hibernate.globally_quoted_identifiers" value="true"/>
            <!--      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />-->
       </properties>

    </persistence-unit>

</persistence>

El archivo persistence.xml se definen las propiedades de la base de datos, como el driver, la URL, el usuario y la contraseña. En el ejemplo anterior las propiedades y las etiquetas principales son:

  • provider: el proveedor de persistencia. En este caso, Hibernate con la clase: org.hibernate.jpa.HibernatePersistenceProvider.
  • jakarta.persistence.jdbc.driver: el driver de la base de datos.
  • jakarta.persistence.jdbc.url: la URL de la base de datos.
  • jakarta.persistence.jdbc.user: el usuario de la base de datos.
  • jakarta.persistence.jdbc.password: la contraseña del usuario de la base de datos.
  • jakarta.persistence.schema-generation.database.action: la acción a realizar sobre la base de datos. En este caso, se crean (create) las tablas de la base de datos.
  • hibernate.show_sql: muestra las sentencias SQL (propio de hibernate).
  • hibernate.format_sql: formatea las sentencias SQL (propio de hibernate).
  • hibernate.highlight_sql: resalta las sentencias SQL (propio de hibernate).
  • hibernate.globally_quoted_identifiers: permite el uso de comillas dobles en las sentencias SQL (pone los nombres de las tablas y columnas entre comillas dobles de manera automática) (propio de hibernate).
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">

    <persistence-unit name="default">
        <!--        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>-->
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:E:/98 - Bases de datos/h2/juego/xogos;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO;DB_CLOSE_DELAY=-1"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="admin"/>
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>

            <property name="eclipselink.logging.level" value="INFO"/>
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>

            <!-- JPA 3.x -->
            <!--            <property name="jakarta.persistence.lock.timeout" value="100"/>-->
            <!--            <property name="jakarta.persistence.query.timeout" value="100"/>-->

            <!-- JPA 2.x -->
            <!--            <property name="javax.persistence.lock.timeout" value="100"/>-->
            <!--            <property name="javax.persistence.query.timeout" value="100"/>-->

        </properties>

    </persistence-unit>
</persistence>

3. Creación de la clase de Entidad Usuario

  1. Precisamos la anotación @Entity para indicar que la clase Usuario es una entidad (obligatoria)
  2. Precisamos la anotación @Id para indicar que el atributo id es la clave primaria (obligatoria)
  3. Usamos la anotación @GeneratedValue para indicar que el valor de la clave primaria se genera automáticamente. Solo cuando las clave primarias son autogeneradas.
  4. Se usa la anotación @Table para indicar que la tabla se llama usuarios (si no deseamos que se llame Usuario).
package com.javhoz.ad.orm;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
@jakarta.persistence.Table(name = "User")
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nombre;
    private String apellidos;
    private String email;
    private String password;

    public Usuario() {
    }
    
    public Usuario(String nombre) {
        this.nombre = nombre;
    }

    public Usuario(String nombre, String apellidos, String email, String password) {
        this.nombre = nombre;
        this.apellidos = apellidos;
        this.email = email;
        this.password = password;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public String getApellidos() {
        return apellidos;
    }

    public void setApellidos(String apellidos) {
        this.apellidos = apellidos;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "id: " + id +
                ", " + nombre + 
                " " + apellidos +
                " (" + email +
                ") " + password + ')' ;
    }
}

4. Creación del EntityManagerFactory y EntityManager

Implementad una clase de utilidad:

package com.javhoz.ad.orm;

import jakarta.persistence.*;

public class JPAUtil {

    // Equivalente a SessionFactory
    private static final EntityManagerFactory ENTITY_MANAGER_FACTORY =
            Persistence.createEntityManagerFactory("default"); // Nombre de la unidad de persistencia

    // Equivalente a Session
    public static EntityManager getEntityManager() {
        return ENTITY_MANAGER_FACTORY.createEntityManager();
    }

    public static void shutdown() {
        ENTITY_MANAGER_FACTORY.close();
    }
}

Otros ejemplos de creación del EntityManagerFactory:

Ejemplo 1 dentro de la clase Main:

package com.javhoz.ad.orm;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class Main {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ejemplopersistenciaJPA");
        EntityManager em = emf.createEntityManager();
    }
}

Podemos emplear un método main para probar la conexión a la base de datos:

package com.javhoz.ad.orm;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class Main {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ejemplopersistenciaJPA");
        EntityManager em = emf.createEntityManager();

        Usuario usuario = new Usuario("Pepe", "Pérez", " ", "1234");
        em.getTransaction().begin();
        em.persist(usuario);
        em.getTransaction().commit();
        System.out.println(usuario);
    }
}

Ejemplo de método setUp():

package com.javhoz.ad.orm;
    
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;

public class TestUsuario {

    private static EntityManagerFactory emf;
    private static EntityManager em;

    @BeforeAll
    static void setUp() {
        emf = Persistence.createEntityManagerFactory("ejemplopersistenciaJPA");
        em = emf.createEntityManager();
    }

    @AfterAll
    static void tearDown() {
        em.close();
        emf.close();
    }
    
    // ...
}

5. Creación del ejemplo de persistencia

package com.javhoz.ad.orm.test;

import com.javhoz.ad.orm.Usuario;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import org.junit.jupiter.api.Test;

public class JPATest {

    @Test
    void jpql() {
        insertData();
        var em = JPAUtil.getEntityManager();

        em.createQuery("select a from Usuario a", Usuario.class)
                .getResultList()
                .forEach(System.out::println);
    }

    @Test
    void criteria() {
        insertData();
        var em = JpaUtil.getEntityManager();

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Usuario> query = cb.createQuery(Usuario.class);
        Root<Usuario> root = query.from(Usuario.class);
        query.select(root);
        em.createQuery(query).getResultList().forEach(System.out::println);

    }

    void insertData(){

        EntityManager em = JpaUtil.getEntityManager();
        em.getTransaction().begin();

        var a1 = new Usuario("a1");
        var a2 = new Usuario("a2");

        em.persist(a1);
        em.persist(a2);

        em.getTransaction().commit();
        em.close();
    }
}

Hasta aquí todo correcto.

Si ejecutamos el método insertData() varias veces, se crean nuevos registros en la base de datos.

Podríamos seguir avanzando y mejorando la arquitectura de la aplicación, pero, por ahora, nos quedaremos aquí.

Podéis echarle un vistazo a los siguientes apartados para ver cómo mejorar la arquitectura de la aplicación.

6. Creación de la clase TestUsuario

package com.javhoz.ad.orm;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.*;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class TestUsuario {

    private static EntityManagerFactory emf;
    private static EntityManager em;

    @BeforeAll
    static void setUp() {
        emf = Persistence.createEntityManagerFactory("ejemplopersistenciaJPA");
        em = emf.createEntityManager();
    }

    @AfterAll
    static void tearDown() {
        em.close();
        emf.close();
    }

    @Test
    void testInsertar() {
        Usuario usuario = new Usuario("Pepe", "Pérez", "    ", "1234");
        em.getTransaction().begin();
        em.persist(usuario);
        em.getTransaction().commit();
        assertNotNull(usuario.getId());
    }
    
    @Test
    void testBuscarPorId() {
        Usuario usuario = em.find(Usuario.class, 1L);
        assertNotNull(usuario);
        assertEquals("Pepe", usuario.getNombre());
    }
    
    @Test
    void testBuscarTodos() {
        List<Usuario> usuarios = em.createQuery("SELECT u FROM Usuario u", Usuario.class).getResultList();
        assertEquals(1, usuarios.size());
    }
    
    @Test
    void testActualizar() {
        Usuario usuario = em.find(Usuario.class, 1L);
        usuario.setNombre("Juan");
        em.getTransaction().begin();
        em.merge(usuario);
        em.getTransaction().commit();
        assertEquals("Juan", usuario.getNombre());
    }
    
    @Test
    void testBorrar() {
        Usuario usuario = em.find(Usuario.class, 1L);
        em.getTransaction().begin();
        em.remove(usuario);
        em.getTransaction().commit();
        Usuario usuarioBorrado = em.find(Usuario.class, 1L);
        assertNull(usuarioBorrado);
    }
}

7. Creación de la clase UsuarioDAO

Un ejemplo un modo más sencillo de implementar el patrón DAO por medio de la clase UsuarioDAO:

package com.javhoz.ad.orm;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.TypedQuery;

import java.util.List;

public class UsuarioDAO {

    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("ejemplopersistenciaJPA");
    
    private EntityManager em;
    
    public UsuarioDAO(EntityManager em) {
        this.em = em;
    }

    public static void insert(Usuario usuario) {
        em.getTransaction().begin();
        em.persist(usuario);
        em.getTransaction().commit();
    }

    public static void delete(Usuario usuario) {
        em.getTransaction().begin();
        em.remove(usuario);
        em.getTransaction().commit();
    }

    public static void update(Usuario usuario) {
        em.getTransaction().begin();
        em.merge(usuario);
        em.getTransaction().commit();
    }

    public static Usuario getById(Long id) {
        return em.find(Usuario.class, id);
    }

    public static List<Usuario> getAll() {
        TypedQuery<Usuario> consulta = em.createQuery("SELECT u FROM Usuario u", Usuario.class);
        List<Usuario> usuarios = consulta.getResultList();
        // También podría ser con CriteriaQuery:
        // CriteriaQuery<Usuario> query = em.getCriteriaBuilder().createQuery(Usuario.class);
        // query.select(query.from(Usuario.class));
        // List<Usuario> usuarios = em.createQuery(query).getResultList();
        
        return usuarios;
    }
}

8. Ejercicio. JPA de una biblioteca

Ejercicio 03.01. Creación de una aplicación de persistencia de una biblioteca

Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección.

Para ello, vamos a crear una clase Book que represente la entidad libro, la clase Contido y otra clase BookDAO que nos permita realizar operaciones básicas CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.

Además, precisamos una clase BibliotecaJpaManager para la gestión y obtención de los objetos de tipo EntityManagerFactory de una manera eficiente. Emplearemos el patrón Singleton para el gestor BibliotecaJpaManager, que tenga un único objeto de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos (queremos que el objeto de tipo EntityManagerFactory sea único para cada unidad de persistencia, para cada unidad de persistencia, no así el EntityManager, que podrá hacer varios para cada unidad de persistencia).

A) BASE DE DATOS (es la misma base de datos que hemos empleado en la unidad de bases de datos con JDBC):

Está formada por una tabla Book y una tabla Contido. La tabla Book tiene una estructura SIMILAR a la siguiente:

Columna Tipo de dato Descripción
idBook int Identificador único del ejemplar del libro
isbn varchar(13) Identificador del libro
titulo varchar(100) Título del libro
autor varchar(100) Autor del libro
anho int Año de publicación del libro
disponible boolean Indica si el libro está disponible
portada Blob Portada del libro en formato binario
dataPublicacion Date Fecha de publicación del libro
-- PUBLIC.Book definition
-- Drop table
-- DROP TABLE PUBLIC.Book;
CREATE TABLE PUBLIC.Book (
	idBook INTEGER NOT NULL AUTO_INCREMENT,
	isbn CHARACTER VARYING(13) NOT NULL,
	titulo CHARACTER VARYING(255) NOT NULL,
	autor CHARACTER VARYING(255),
	anho INTEGER,
	disponible BOOLEAN DEFAULT TRUE,
	portada BINARY LARGE OBJECT,
	dataPublicacion DATE,
	CONSTRAINT BOOK_PK PRIMARY KEY (idBook)
);
CREATE UNIQUE INDEX IdBookPK ON PUBLIC.Book (idBook);
CREATE INDEX IdxBookISBN ON PUBLIC.Book (isbn);
CREATE INDEX IdxBookTitle ON PUBLIC.Book (titulo);
CREATE UNIQUE INDEX PRIMARY_KEY_93 ON PUBLIC.Book (idBook);

La tabla Contido tiene una estructura SIMILAR a la siguiente:

Columna Tipo de dato Descripción
idContido int Identificador único del contenido del libro
idBook int Identificador del libro
contido Blob Contenido del libro en formato binario

*idBook es una clave foránea+ que referencia a la tabla Book.

-- PUBLIC.Contido definition
-- Drop table
-- DROP TABLE PUBLIC.Contido;

CREATE TABLE PUBLIC.Contido (
	idContido INTEGER NOT NULL AUTO_INCREMENT,
	idBook INTEGER NOT NULL,
	contido CHARACTER LARGE OBJECT,
	CONSTRAINT Contido_PK PRIMARY KEY (idContido)
);
CREATE INDEX FK_ID_BOOK_INDEX_9 ON PUBLIC.Contido (idBook);
CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC.Contido (idContido);

-- PUBLIC.Contido foreign keys
ALTER TABLE PUBLIC.Contido ADD CONSTRAINT FK_ID_BOOK FOREIGN KEY (idBook) REFERENCES PUBLIC.Book(idBook) ON DELETE CASCADE ON UPDATE CASCADE;

Parámetros de la base de datos:

DRIVER: "org.h2.Driver"
URL: "jdbc:h2:rutaBaseDatosSinExtensión;DB_CLOSE_ON_EXIT=TRUE;FILE_LOCK=NO;DATABASE_TO_UPPER=FALSE"

El fichero persistencia.xml debe tener la siguiente configuración:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="bibliotecaH2" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>-->
        <exclude-unlisted-classes>false</exclude-unlisted-classes> <-- false si no se listan las clases en el archivo de configuración -->
        <properties>
            <!--      <property name="jakarta.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/peliculas"/>-->
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:rutaALaBaseDeDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/>
            <!-- Ejemplo con Access -->
            <!--<property name="jakarta.persistence.jdbc.url" value="jdbc:ucanaccess://rutabase_base_datos.mdb"/>-->
            <!--      <property name="jakarta.persistence.jdbc.user" value="root"/>-->
            <!--      <property name="jakarta.persistence.jdbc.password" value=""/>-->
            <property name="jakarta.persistence.jdbc.user" value=""/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <!--      <property name="jakarta.persistence.jdbc.driver" value="net.ucanaccess.jdbc.UcanaccessDriver"/>-->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <!-- Automáticamente, genera el esquema de la base de datos -->
            <property name="jakarta.persistence.schema-generation.database.action" value="none"/>

            <!-- Muestra por pantalla las sentencias SQL -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.highlight_sql" value="true"/>
            <!--      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />--> <!-- para HSQLDB y Ucanaccess -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        </properties>
    </persistence-unit>
</persistence>

B) Clase BibliotecaJpaManager:

Mediante el patrón Singleton crea una clase BibliotecaJpaManager, mediante el patrón Singleton de manera que tenga un atributo emFactory de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos.

Además, debe tener un método estático getEntityManager que devuelva un objeto de tipo EntityManager y que se encargue de crear el objeto EntityManager.

Hazlo con Thread-Safe y doble comprobación.

Reto: haz que la clase BibliotecaJpaManager tenga un singleton para cada factory, guardándolos en un mapa con el nombre de la unidad de persistencia como clave:

private static Map<String, EntityManagerFactory> instancies = new HashMap<>();

C) Clase Book implementa Serializable:

Haz que sea una entidad JPA y que implemente la interfaz Serializable.

La clase Book debe tener los siguientes atributos:

  • idBook: Long (autonumérico)
  • isbn: String (tamaño 13)
  • title: String
  • author: String
  • ano: Integer
  • available: Boolean
  • portada: byte[]
  • dataPublicacion: LocalDate (Nuevo campo)
  • List<Contido> contenido; (Nuevo, lista de contenidos del libro, de momento, mientras no tengamos relaciones, hazlo transient)

(Fíjate que ya no existe el campo contido[] que habíamos definido en la clase Book de la unidad de bases de datos con JDBC).

La clase debe tener, al menos, los siguientes constructores:

  • Book()
  • Book(String isbn, String title, String author, Short year, Boolean available, byte[] portada)
  • Book(Long idBook, String isbn, String title, String author, Short year, Boolean available, byte[] portada)
  • Aquellos que consideres necesarios.

La lista de Contido es una lista de objetos de tipo Contido que representan los contenidos del libro. La clase Contido tiene los siguientes atributos: idContido y contido. Ten en cuenta que existe en la base de datos una tabla Contido con los campos idContido y contido y una referencia al libro mediante una clave foránea idBook. De momento, no incluyas la List de contenidos en la clase Book, hazlos transient (bien con la anotación @Transient o con la palabra reservada transient), hasta que veamos las relaciones, que será @OneToMany.

Los métodos “set” de las propiedades deben devolver una referencia al propio objeto para poder encadenarlos.

IMPORTANTE: ten en cuenta que los atributos de la clase Book no coinciden con los campos de tabla por lo que debes refactorizar: author -> autor, ano -> anho, avaliable -> disponible, … o emplear la anotación @Column para mapear los atributos de la clase con los campos de la tabla.

Métodos de la clase Book (ya implantados):

  • Get y set para cada atributo.

  • setPortada (sin implantar): recibe File y lo asigna al atributo portada.

  • setPortada (sin implantar): recibe un array de bytes y lo asigna al atributo portada.

  • setPortada (Sin implantar): recibe un String con el nombre del fichero y lo asigna al atributo portada.

  • getImage: devuelve un objeto de tipo Image con la portada del libro.

    public Image getImage() {
    if (portada != null) {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(portada)) {
            return ImageIO.read(bis);
        } catch (IOException e) {
        }
    }
    return null;
}
  • equals y hashCode: considerando que son iguales cuando tienen el mismo isbn. Además, el método hashCode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).

  • toString: devuelve el título, el autor y el año. Si no está disponible escribe un asterisco.

D) Clase Contido implementa Serializable:

A diferencia de la clase empleada en la unidad de bases de datos con JDBC, la clase Contido no debe tener referencia al idBook, pues no es la mejor práctica (está hecho sólo a modo de ejemplo), debe tener, si queremos la relación bidireccional, una referencia a Book.

  • idContido: Long (autonumérico)
  • contido: String (contenido del libro en formato texto). Puedes hacer un atributo de tipo String o byte[] (para almacenar el contenido en formato binario), en cualquier caso, deberías modificar la tabla Contido en la base de datos.
  • Book book (relación con la clase Book)

Si has implantado la clase ContidoDao, debes modificar los métodos que obtienen el idBook del book:

contido.getBook().getIdBook();

E) Clase BookJPADao:

Esta clase, al igual que la clase BookDao, la clase BookJPADaodebe implantar la interface Dao<T>, de modo que tenga un objeto de tipo EntityManagercomo atributo. En sistemas empresariales, como la gestión de transacciones no se suele hacer por método, se guarda una referencia a la clase EntityManagerFactory y se gestiona por medio de try-with-resources para manejar los cierres de los EntityManager.

Dao<T>:

import java.util.List;

/**
 *
 * @author pepecalo
 * @param <T> Tipo de dato del objeto
 */
public interface DAO<T> {

    T get(long id);

    List<T> getAll();

    void save(T t);

    void update(T t);
   
    void delete(T t);

    public boolean deleteById(long id);

    public List<Integer> getAllIds();

    public void updateLOB(T book, String f); // en BookJPADao recibe un objeto de tipo Book y un String con el nombre del fichero

    public void updateLOBById(long id, String f);
    
    void deleteAll();
}

Clase BookJPADao:

Implementa la interfaz DAO<Book> y gestiona las operaciones CRUD sobre la tabla Book de la base de datos. Tiene como atributo un objeto de tipo EntityManager que recoge en el constructor.

Clase BookDAOFactory:

Factory de clases que implanten la interfaz DAO<Book>.

import jakarta.persistence.EntityManager;

/**
 * Factory de clases que implanten la interfaz DAO<Book>.
 * 
 * @version 1.0
 * @since 1.0
 * @see BookJpaDAO
 * @see TipoDAO
 */

public class BookDaoFactory {
    
    public enum TipoDao {
        JDBC_H2, JPA_H2, JPA_POSTGRES, HIBERNATE, JSON, JDBC_POSTGRES;
    }

    public static Dao<Book> getBookDAO(TipoDao tipo) {
        switch (tipo) {
            // ..
        }
        return null;
    }
}

Implementa un método estático getBookDAO que recoge el tipo de DAO que se va a emplear y devuelve el objeto de tipo BookJPADAO. Sería interesante hacer cambios para que getBookDao recoja los parámetros necesarios como propiedades de la base de datos, nombre del archivo JSON, nombre de la unidad de persistencia, etc.

public static Dao<Book> getBookDAO(TipoDao tipo, Map<String, String> propiedades) {
    switch (tipo) {
        case JPA_H2:
            return new BookJPADao(BibliotecaJpaManager.getEntityManager(propiedades.get("unidadPersistencia")));
        // ...
        default:
            return null;
    }
    
}

AppBiblioteca:

Ejecuta la aplicación para que haga uso del BookDaoFactory para obtener un objeto de tipo DAO<Book> para asignarlo al controlador de la aplicación. La aplicación debe funcionar igual que con JDBC, pero ahora con JPA.

Con JDBC_H2:

Dao<Book> bookDao = BookDaoFactory.getBookDAO(BookDaoFactory.TipoDAO.JDBC_H2);

Con JPA_H2:

Dao<Book> bookDao = BookDaoFactory.getBookDAO(BookDaoFactory.TipoDAO.JPA_H2);

Haz pruebas con los dos tipos de DAO. ¿Has notado alguna diferencia? Haz mejoras sobre el funcionamiento de la aplicación.

Puedes hacer pruebas de persistencia de libros en la base de datos:

Book libro = new Book("9788424937744", "Tractatus logico-philosophicus-investigaciones filosóficas", "Ludwig Wittgenstein", 2017, false);
libro = new Book("9788499088150", "Verano", "J. M. Coetzee", 2011, true);

8.1. Solución

Solución: BibliotecaJpaManager
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;
import java.util.Map;

import static com.javhoz.ad.biblioteca.model.BibliotecaLogger.LOG;

public class BibliotecaJpaManager {

    public static final String BIBLIOTECA_H2 = "bibliotecaH2";
    public static final String BIBLIOTECA_POSTGRES = "bibliotecaPostgres";


    private static final Map<String, EntityManagerFactory> instancies = new HashMap<>();

    private BibliotecaJpaManager() {
    }

    private static boolean isEntityManagerFactoryClosed(String unidadPersistencia) {
        return !instancies.containsKey(unidadPersistencia) || instancies.get(unidadPersistencia) == null ||
                !instancies.get(unidadPersistencia).isOpen();
    }

    public static EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        if (isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (BibliotecaJpaManager.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    try {
                        instancies.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                    } catch (Exception e) {
                        LOG.error("Erro ó crear a unidade de persistencia " + unidadPersistencia +
                                ": " + e.getMessage());
                    }
                }
            }
        }
        return instancies.get(unidadPersistencia);
    }


    public static EntityManager getEntityManager(String persistenceUnitName) {
        return getEntityManagerFactory(persistenceUnitName).createEntityManager();
    }


    public static void close(String persistenceUnitName) {
        if (instancies.containsKey(persistenceUnitName)) {
            instancies.get(persistenceUnitName).close();
            instancies.remove(persistenceUnitName);
        }
    }

}
Solución: Book
import jakarta.persistence.*;

import javax.imageio.ImageIO;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.Objects;

/**
 * @author pepecalo
 */
@Entity
public class Book implements Serializable {

    //    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idBook;
    @Column(length = 13, nullable = false, unique = true))
    private String isbn;
    @Column(name = "titulo", nullable = false)
    private String title;
    @Column(name = "autor")
    private String author;
    @Column(name = "anho")
    private Short ano;
    @Column(name = "disponible")
    private Boolean available;
    private byte[] portada;

    private LocalDate dataPublicacion;

//    @Transient // Ambas opciones son válidas
    transient private List<Contido> contenido = new ArrayList<>();

    private static final long serialVersionUID = 1L;

    public Book() {
    }

    public Book(String title, String author, Short year, Boolean available) {
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
    }

    public Book(String isbn, String title, String author, Short year,
                Boolean available) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
    }

    public Book(String isbn, String title, String author, Short year,
                Boolean available, byte[] portada) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
        this.portada = portada;
    }

    public Book(Long idBook, String isbn, String title, String author,
                Short year, Boolean available, byte[] portada) {
        this.idBook = idBook;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
        this.portada = portada;
    }

    public Long getIdBook() {
        return idBook;
    }

    public Book setIdBook(Long idBook) {
        this.idBook = idBook;
        return this;
    }

    public String getIsbn() {
        return isbn;
    }

    public Book setIsbn(String isbn) {
        this.isbn = isbn;
        return this;
    }

    public String getTitle() {
        return title;
    }

    public Book setTitle(String title) {
        this.title = title;
        return this;
    }

    public String getAuthor() {
        return author;
    }

    public Book setAuthor(String author) {
        this.author = author;
        return this;
    }

    public Short getYear() {
        return ano;
    }

    public Book setAno(Short ano) {
        this.ano = ano;
        return this;
    }

    public Boolean isAvailable() {
        return available;
    }

    public Book setAvailable(Boolean available) {
        this.available = available;
        return this;
    }

    public byte[] getCover() {
        return portada;
    }

    public Book setCover(byte[] portada) {
        this.portada = portada;
        return this;
    }

    public LocalDate getDataPublicacion() {
        return dataPublicacion;
    }

    public Book setDataPublicacion(LocalDate dataPublicacion) {
        this.dataPublicacion = dataPublicacion;
        return this;
    }

    /**
     * Asigna la portada con flujos, leyendo los bytes.
     *
     * @param f
     */
    public Book setPortada(File f) {
        if (f == null || !f.exists())
            return this;
        Path p = Paths.get(f.getAbsolutePath());
        try (BufferedInputStream bi = new BufferedInputStream(Files.newInputStream(p));
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesLidos;
            while ((bytesLidos = bi.read(buffer)) > 0) {
                outputStream.write(buffer, 0, bytesLidos);
            }

            portada = outputStream.toByteArray();
        } catch (FileNotFoundException ex) {
            System.err.println("Archivo no encontrado: " + ex.getMessage());
        } catch (IOException ex) {
            System.err.println("Erro de E/S: " + ex.getMessage());
        }
        return this;
    }

    /**
     * Asigna la portada con Java NIO, leyendo los bytes.
     *
     * @param file
     */
    public Book setPortada(String file) {
        try {
            Path ruta = Paths.get(file);
            portada = Files.readAllBytes(ruta);
        } catch (IOException ex) {
            System.err.println("Error de E/S: " + ex.getMessage());
        }
        return this;
    }

    public Image getImage() {
        if (portada != null) {
            try (ByteArrayInputStream bis = new ByteArrayInputStream(portada)) {
                Image imaxe = ImageIO.read(bis);
                if(available) {
                    imaxe.getGraphics().drawLine(0,0, 100, 100);
                }

                return imaxe;
            } catch (IOException e) {
            }
        }
        return null;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 41 * hash + Objects.hashCode(this.isbn);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final Book other = (Book) obj;
        return Objects.equals(this.isbn, other.isbn);
    }

    @Override
    public String toString() {
        return idBook + "] [isbn: " + isbn + "] " + title + ". "
                + author + " (" + ano + ") [" + ((available) ? '*' : ' ') + ']';
    }

}
Solución: Clase Contido

De momento, no hemos declarado Contido como entidad JPA, pero lo haremos en el futuro, cuando veamos las relaciones.

import java.util.Objects;

/**
 * @autor pepecalo
 * CREATE TABLE PUBLIC.Contido (
 * 	idContido INTEGER NOT NULL AUTO_INCREMENT,
 * 	idBook INTEGER NOT NULL,
 * 	contido CHARACTER LARGE OBJECT,
 * 	CONSTRAINT Contido_PK PRIMARY KEY (idContido),
 * 	CONSTRAINT FK_ID_BOOK FOREIGN KEY (idBook) REFERENCES PUBLIC.Book(idBook) ON DELETE CASCADE ON UPDATE CASCADE
 * );
 * CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC.Contido (idContido);
 */

public class Contido {

    private Long idContido;
    private String contido;
    private Book book;

    public Contido() {
    }

    public Contido(Long idBook, String contido) {
        this.contido = contido;
    }

    public Contido(Long idContido, Long idBook) {
        this.idContido = idContido;
    }

    public Contido(Long idContido, Long idBook, String contido) {
        this.idContido = idContido;
        this.contido = contido;
    }

    public Long getIdContido() {
        return idContido;
    }

    public void setIdContido(Long idContido) {
        this.idContido = idContido;
    }

    public String getContido() {
        return contido;
    }

    public void setContido(String contido) {
        this.contido = contido;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    @Override
    public int hashCode() {
        return 97 * 7 + Objects.hashCode(this.idContido);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Contido other)) return false;
        return Objects.equals(this.idContido, other.idContido);
    }

    @Override
    public String toString() {
        return idContido + ": " + contido;
    }
}
Solución: BookJPADao
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.TypedQuery;

import java.util.List;

public class BookJPADao implements Dao<Book> {

    private final EntityManager em;

    public BookJPADao(EntityManager em) {
        this.em = em;
    }

    @Override
    public Book get(long id) {
        return em.find(Book.class, id);
    }

    @Override
    public List<Book> getAll() {
        TypedQuery<Book> query = em.createQuery("SELECT b FROM Book b", Book.class);
        return query.getResultList();
    }

    @Override
    public void save(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.persist(book);
        tx.commit();
    }

    @Override
    public void update(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.merge(book);
        tx.commit();
    }

    @Override
    public void delete(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.remove(book);
        tx.commit();
    }

    @Override
    public boolean deleteById(long id) {
        Book book = get(id);
        if (book != null) {
            delete(book);
            return true;
        }
        return false;
    }

    @Override
    public List<Integer> getAllIds() {
        TypedQuery<Integer> query = em.createQuery("SELECT b.idBook FROM Book b", Integer.class);
        return query.getResultList();
    }

    @Override
    public void updateLOB(Book book, String f) {
        book.setPortada(f);
        update(book); // La tansacción se hace en el método update
    }

    @Override
    public void updateLOBById(long id, String f) {
        Book book = get(id);
        if (book != null) {
            updateLOB(book, f); // La transacción se hace en el método updateLOB, que a su vez llama a update
        }
    }

    @Override
    public void deleteAll() {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.createQuery("DELETE FROM Book").executeUpdate();
        tx.commit();
    }
}
Solución: BookDaoFactory
import java.util.Map;

public class BookDaoFactory {
    public enum TipoDAO {
        JDBC_H2, JPA_H2, JPA_POSTGRES, HIBERNATE, JSON, JDBC_POSTGRES;
    }

    public static Dao<Book> getBookDAO(TipoDAO tipo) {
        switch (tipo) {
            case JDBC_H2:
                BibliotecaConnectionMaganer bibliotecaConnection = BibliotecaConnectionMaganer.getInstance();
                return new BookDao(bibliotecaConnection.getConnection());
            case JPA_H2:
                return new BookJPADao((BibliotecaJpaManager.getEntityManager(BibliotecaJpaManager.BIBLIOTECA_H2)));
                // ...
        }
        return null;
    }
}
Última actualización: 23.09.2025

04. Gestión de entidades con EntityManager.

1. EntityManager

El gestor de entidades (EntityManager) es el encargado de gestionar el ciclo de vida de las entidades. Con él podemos persistir (persist), actualizar, eliminar (remove) y recuperar (find) entidades, así como realizar consultas (createQuery).

  • Contexto de persistencia (Persistence Context): es conjunto de instancias de entidad gestionadas dentro de un gestor de entidades (EntityManager) en un momento dado.

  • Es necesario invocar una llamada de API específica antes de que una entidad se persista realmente en la base de datos.

  • Las llamadas de API para las operaciones en entidades, implementada por el gestor de entidades, se encapsula casi por completo dentro de una única interfaz jakarta.persistence.EntityManager, gestor de entidades al que se le delega el trabajo real de la persistencia.

  • Hasta que se utilice un gestor de entidades para crear, leer o escribir realmente una entidad, la entidad no es más que un objeto Java regular (no persistente). Se dice que ese objeto está gestionado por el EntityManager (gestor de entidades9.

  • Sólo puede existir una instancia Java con la misma identidad persistente en un contexto de persistencia en cualquier momento (con un único ID).

Consejo

Las implementaciones concretas de la interface EntityManager permiten leer y escribir en una base de datos específica, y ser implementadas por un proveedor de persistencia particular (o simplemente proveedor).

Es el proveedor es el que suministra el motor de implementación de respaldo para toda la API de Persistencia de Jakarta, desde el EntityManager hasta la implementación de las clases de consulta y la generación de SQL.

Para obtener un gestor de entidades, se debe crear una instancia de la fábrica de gestores de entidades, del tipo jakarta.persistence.EntityManagerFactory.

Cada EntityManager gestiona una unidad de persistencia. Una unidad de persistencia dicta de manera implícita o explícita la configuración y las clases de entidad utilizadas por todos los gestores de entidades obtenidos de la única instancia de EntityManagerFactory vinculada a esa unidad de persistencia. Por lo tanto, hay una correspondencia uno a uno entre una unidad de persistencia y su instancia concreta de EntityManagerFactory.

Objetos, Clases y Conceptos de la API

Objeto API Descripción del Objeto
Persistence Persistence Clase de inicio utilizada para obtener una fábrica de gestores de entidades (EntityManagerFactory)
Entity Manager Factory EntityManagerFactory Objeto Factory configurado utilizado para obtener gestores de entidades (EntityManager)
Persistence Unit Configuración con nombre que declara las clases de entidad y la información de la base de datos
Entity Manager EntityManager Objeto principal de la API utilizado para realizar operaciones y consultas en entidades
Persistence Context Conjunto de todas las instancias de entidad gestionadas por un gestor de entidades específico

2. Creación de un EntityManager

Un gestor de entidades siempre se obtiene de una EntityManagerFactory.

En el entorno de Java SE, podemos utilizar una clase de llamada Persistence invocando al método estático createEntityManagerFactory() de la clase Persistence que devuelve el EntityManagerFactory para el nombre de la unidad de persistencia especificado. Por ejemplo, para una unidad de persistencia llamada ServicioEmpleado:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("ServicioEmpleado");

El nombre de la unidad de persistencia especificada, “ServicioEmpleado”, pasado al método createEntityManagerFactory(), identifica la configuración de la unidad de persistencia dada que determina cosas como los parámetros de conexión que los gestores de entidades creados a partir de ese objeto Factory utilizarán al conectarse a la base de datos.

Se puede obtener fácilmente un gestor de entidades de ella:

EntityManager em = emf.createEntityManager();

Un modo muy usual de crear un gestor de entidades es por medio de una clase Singleton:

public class EMF {
    private static final EntityManagerFactory emfInstance = Persistence.createEntityManagerFactory("ServicioEmpleado");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

Que se puede utilizar de la siguiente manera:

EntityManager em = EMF.get().createEntityManager();

Más interesante es el uso de patrón Singleton con Thread-Save y Lazy-Initialization para obtener un EntityManagerFactory:

public class EMF {
    private static volatile EntityManagerFactory emfInstance;

    private EMF() {}

    public static EntityManagerFactory get() {
        if (emfInstance == null) {
            synchronized (EMF.class) {
                if (emfInstance == null) {
                    emfInstance = Persistence.createEntityManagerFactory("ServicioEmpleado");
                }
            }
        }
        return emfInstance;
    }
    //...
}

3. Operaciones CRUD

Veremos ejemplos básicos de cómo realizar las operaciones CRUD (Create, Read, Update, Delete) con JPA. y una clase Empleado.

Ejemplo de Entidad Empleado:

@Entity
public class Empleado {
 @Id private int id;
 private String nome;
 private long salario;
 public Empleado() {}
 public Empleado(int id) { this.id = id; }
 public int getId() { return id; }
 public void setId(int id) { this.id = id; }
 public String getNome() { return nome; }
 public void setNome(String nome) { this.nome = nome; }
 public long getSalario() { return salario; }
 public void setSalario (long salario) { this.salario = salario; }
}

3.1. Persistir una entidad

Persistir una entidad es la operación de tomar una entidad transitoria, o una que aún no tiene ninguna representación persistente en la base de datos, y almacenar su estado para que pueda ser recuperado más tarde.

Empleado emp = new Empleado(158); // Crea una instancia de la entidad Empleado
em.persist(emp);
  • Creamos un objeto de tipo Empleado configurando el ID, no el nombre ni el salario del Empleado.

  • Llamamos a persist() para iniciar la persistencia en la base de datos.

Si el gestor de entidades encuentra un error lanzará una excepción no verificada de tipo PersistenceException.

Cuando se completa la llamada a persist(), emp se convertirá en una entidad gestionada dentro del contexto de persistencia del gestor de entidades.

Ejemplo de un método sencillo que crea un nuevo empleado y lo persiste en la base de datos.

public Empleado createEmpleado(int id, String nome, long salario) {
    Empleado emp = new Empleado(id);
    emp.setNome(nome);
    emp.setSalario(salario);
    em.persist(emp);
    return emp;
}

3.2. Obtención de una entidad

Una vez que una entidad está en la base de datos, lo siguiente que normalmente se quiere hacer es obtenerla de nuevo:

Empleado emp = em.find(Empleado.class, 158);

El método find():

<T> T find (Class<T> entityClass, Object primaryKey)

Recoge la clase de la entidad que se está buscando (Empleado), permite que el método find sea parametrizado y devuelva un objeto del mismo tipo, y el objeto con ID o clave primaria que identifica la entidad en particular (con id 158).

Con esta información el gestor de entidades encuentra la instancia en la base de datos y el empleado que se devuelve será una entidad gestionada, lo que significa que existirá en el contexto de persistencia actual asociado con el gestor de entidades.

En el caso de que el objeto no se encuentre la llamada a find() simplemente devuelve null. Debe realizarse una comprobación de nulos antes de la próxima vez que se utilice la variable emp.

Método de búsqueda:

public Empleado findEmpleado(int id) {
    return em.find(Empleado.class, id);
}

3.3. Eliminación de una entidad

Aunque podría parecer lo contrario, el borrado (DELETE) de entidad de la base de datos no demasiado común.
Muchas aplicaciones nunca eliminan objetos, o si lo hacen, simplemente marcan los datos como obsoletos o ya no válidos y los mantienen fuera de la vista de los clientes.

Para eliminar una entidad debe estar gestionada, debe estar presente en el contexto de persistencia.

La aplicación que realiza la llamada ya debería haber cargado o accedido a la entidad y ahora está emitiendo una sentencia para eliminarla.

Puede hacerse por medio del método remove:

void remove (Object entity)
Empleado emp = em.find(Empleado.class, 158);
em.remove(emp);

El método find() devuelve una instancia gestionada de Empleado, y luego se elimina la entidad usando la llamada remove() en el gestor de entidades.

Si la entidad no se encuentra, entonces el método find() devolverá null, resultando una java.lang.IllegalArgumentException.
Se debe incluir una verificación de nulidad antes de llamar a remove():

public void removeEmpleado(int id) {
    Empleado emp = em.find(Empleado.class, id);
    if (emp != null) {
        em.remove(emp);
    }
}

3.4. Actualización de una entidad

La actualización de una entidad es la operación de tomar una entidad gestionada y modificar su estado para que se refleje en la base de datos.

Empleado emp = em.find(Empleado.class, 158);
emp.setSalario(1000000);

Existen varias formas de actualizar una entidad, pero por ahora veremos el caso más simple y común, cuando se dispone de una entidad gestionada y se desea realizar cambios en ella.

Si no tenemos una referencia a la entidad gestionada:

  1. Debemos obtener la entidad una usando find().
  2. Realizar operaciones de modificación en la entidad gestionada.

El siguiente código agrega 1000 euros al salario del empleado con un ID de 158 (yo ;-)):

Empleado emp = em.find(Empleado.class, 158);
emp.setSalario(emp.getSalario() + 1000);

No se llama al gestor de entidades para modificar el objeto, sino accediendo al objeto en sí.

Por esta razón, es importante que la entidad sea una instancia gestionada; de lo contrario, el proveedor de persistencia no tendrá medios para detectar el cambio y no se realizarán cambios en la representación persistente del empleado.

public Empleado raiseSalarioEmpleado(int id, long cantidad) {
        Empleado emp = em.find(Empleado.class, id);
    if (emp != null) {
        emp.setSalario(emp.getSalario() + cantidad);
    }
    return emp;
}

Si no pudimos encontrar al empleado, devolvemos null para que el llamador sepa que no se pudo realizar ningún cambio. Indicamos el éxito devolviendo al empleado actualizado.

4. Transacciones

En los ejemplos anteriores, no se ha hecho referencia a las transacciones, aunque los cambios en las entidades deben hacerse persistentes mediante una transacción.

Excepto find(), asumimos que cada método estaba envuelto en una transacción.

La llamada a find() no es una operación de mutación, por lo que puede llamarse en cualquier momento, con o sin una transacción.

En estos ejemplos estamos empleando un entorno de Java SE, y el servicio de transacciones que debe usarse en Java SE es jakarta.persistence.EntityTransaction necesitamos comenzar y confirmar la transacción en los métodos operativos, o necesitamos comenzar y confirmar la transacción antes y después de llamar a un método operativo.

Inicio de la transacción:

En ambos casos, se inicia una transacción llamando a getTransaction() en el entity manager para obtener la EntityTransaction e invocando begin() en ella:

EntityTransaction tx = em.getTransaction();
tx.begin();

Para confirmar la transacción, se invoca a commit() en el objeto EntityTransaction obtenido del entity manager.

Ejemplo completo:

em.getTransaction().begin();
createEmpleado(158, "John Doe", 45000);
em.getTransaction().commit();
Jakarta EE vs Java SE

La clave del uso de transacciones es el entorno en el que se ejecuta el código.

La situación típica al ejecutarse dentro del entorno del contenedor Jakarta EE utiliza el API estándar de Transacciones de Jakarta. El modelo de transacción cuando se ejecuta en el contenedor es asumir que la aplicación se encargará de que exista un contexto transaccional cuando sea necesario.

Si no hay una transacción presente, entonces la operación de modificación lanzará una excepción o el cambio simplemente no se persistirá en el almacén de datos.

5. Consultas

Una consulta es una solicitud de datos. En el contexto de JPA, una consulta es una solicitud de entidades.

Las consultas se pueden realizar de dos maneras:

  • Consultas dinámicas: se construyen en tiempo de ejecución como cadenas de consulta.
  • Consultas con nombre: se definen en tiempo de compilación como consultas con nombre.

5.1. Consultas dinámicas

Las consultas dinámicas se construyen en tiempo de ejecución como cadenas de consulta. Las cadenas de consulta son sentencias de consulta en lenguaje de consulta de entidades (JPQL).

El lenguaje de consulta de entidades (JPQL) es un lenguaje de consulta orientado a objetos que se utiliza para definir consultas de entidades y sus resultados.

Las consultas dinámicas se crean utilizando el método createQuery() en el gestor de entidades:

Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000");

El método createQuery() toma una cadena de consulta JPQL y devuelve un objeto Query que se puede utilizar para ejecutar la consulta y recuperar los resultados.

5.2. Consultas con nombre (estáticas)

Las consultas con nombre se definen en tiempo de compilación como consultas con nombre. Las consultas con nombre se definen en un archivo de metadatos de la entidad o en un archivo de metadatos de consulta.

Las consultas con nombre se crean utilizando el método createNamedQuery() en el gestor de entidades:

Query q = em.createNamedQuery("findEmpleadoPorSalario");

El método createNamedQuery() toma el nombre de la consulta y devuelve un objeto Query que se puede utilizar para ejecutar la consulta y recuperar los resultados.

Ejemplo de creación de una consulta con nombre:

@Entity
@NamedQuery(name="findEmpleadoPorSalario", query="SELECT e FROM Empleado e WHERE e.salario > 100000")
public class Empleado {
    //...
}

5.3. Ejecución de consultas

Una vez que se ha creado una consulta, se puede ejecutar utilizando el método getResultList() o getSingleResult():

TypedQuery<Empleado> q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000", Empleado.class);
List<Empleado> results = q.getResultList();

Existen consultas tipadas y no tipadas. Las consultas tipadas devuelven un tipo específico de entidad, mientras que las consultas no tipadas devuelven un tipo de entidad genérico.

Con un consulta no tipada sería:

Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.salario > 100000");
List results = q.getResultList();

Si la consulta no devuelve ningún resultado, getResultList() devuelve una lista vacía y getSingleResult() lanza una excepción NoResultException. Si el resultado no es único y devuelve más de un resultado, getSingleResult() lanza una excepción NonUniqueResultException.

5. Consultas (ampliado)

En Jakarta Persistence, una consulta es similar a una consulta de base de datos, excepto que en lugar de utilizar Structured Query Language (SQL) para especificar los criterios de la consulta, estamos consultando sobre entidades y utilizando un lenguaje llamado Jakarta Persistence Query Language (Jakarta Persistence QL).

Una consulta se implementa en código como un objeto Query o TypedQuery<X>.

Se construye utilizando el EntityManager como fábrica.

La interfaz EntityManager incluye una variedad de llamadas a la API que devuelven un nuevo objeto Query o TypedQuery<X>.

Tipos de consultas en Jakarta Persistence

Una consulta puede definirse de forma estática o dinámica.

  1. Una consulta estática (con nombre) se define típicamente en metadatos de anotación o XML, y debe incluir los criterios de la consulta, así como un nombre asignado por el usuario. Este tipo de consulta también se llama consulta nombrada y se busca posteriormente por su nombre en el momento de su ejecución.

  2. Una consulta dinámica puede emitirse en tiempo de ejecución proporcionando los criterios de consulta de Jakarta Persistence QL o un objeto de criterios. Pueden ser un poco más costosas de ejecutar porque el proveedor de persistencia no puede realizar ninguna preparación de consulta de antemano, pero las consultas de Jakarta Persistence QL son, no obstante, muy simples de usar y pueden emitirse en respuesta a la lógica del programa o incluso la lógica del usuario.

El siguiente ejemplo muestra cómo crear una consulta dinámica:

(Nota: por supuesto, esta puede no ser una consulta muy buena para ejecutar si la base de datos es grande y contiene cientos de miles de empleados, pero sigue siendo un ejemplo adecuado):

Ejemplo usando getResultList:

TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
List<Empleado> emps = query.getResultList();

Ejemplo usando getResultStream:

TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
Stream<Empleado> employee = query.getResultStream();

Creamos un objeto TypedQuery<Empleado> emitiendo la llamada createQuery() en el EntityManager y pasando la cadena de Jakarta Persistence QL que especifica los criterios de la consulta, así como la clase que debería ser parametrizada en la consulta.

La cadena de Jakarta Persistence QL no se refiere a una tabla de base de datos EMPLEADO, sino a la entidad Empleado, por lo que esta consulta selecciona todos los objetos Empleado sin filtrarlos más.

Para ejecutar la consulta, simplemente invocamos el método getResultList() o el método getResultStream() en ella.

  • El método getResultList() devuelve un List<Empleado> que contiene los objetos Empleado que coincidieron con los criterios de la consulta. Observa que el List está parametrizado por Empleado, ya que el tipo parametrizado se propaga desde el argumento de clase inicial pasado al método createQuery(). Podemos crear fácilmente un método que devuelva todos los empleados.

  • El método getResultStream() devuelve un flujo del resultado de la consulta, por lo que, en este caso, devuelve el flujo del resultado de la consulta Empleado. Por defecto, delega en getResultList().stream().

El método getResultStream() proporciona una mejor manera de moverse a través del conjunto de resultados de la consulta, ya que, para conjuntos de datos grandes, evita leer todo el “conjunto de resultados” en memoria antes de que pueda usarse en la aplicación.

Método para Emitir una Consulta

public List<Empleado> findAllEmpleados() {
    TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
    return query.getResultList();
}

Con Stream:

public Stream<Empleado> findAllEmpleadosStream() {
    TypedQuery<Empleado> query = em.createQuery("SELECT e FROM Empleado e", Empleado.class);
    return query.getResultStream();
}

que podríamos usar de la siguiente manera:

Stream<Empleado> empleadosStream = findAllEmpleadosStream();
empleadosStream.forEach(System.out::println);

Ejercicios

Ejercicio 04.01. Descarga y creación de la base de datos de JokeAPI

Dado el modelo de la aplicación de JokeAPI, en la que tenemos las enumeraciones Categoriam TipoChiste, Flag y la clase Chiste, vamos a crear una base de datos con JPA y los chistes de la API.

Enumeraciones

A) La enumeración Categoria tiene los siguientes valores:

public enum Categoria {
    ANY("Any"),
    MISC("Misc"),
    PROGRAMMING("Programming"),
    DARK("Dark"),
    PUN("Pun"),
    SPOOKY("Spooky"),
    CHRISTMAS("Christmas");
    //...
}
Detalle de implementación de la enumeración Categoría
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * <p>
 * Enumeración de categorías de chistes.
 * Pueden ser: Any, Misc, Programming, Dark, Pun, Spooky, Christmas
 * Atributo: nombre, de tipo cadena.
 */
public enum Categoria {
    ANY("Any"),
    MISC("Misc"),
    PROGRAMMING("Programming"),
    DARK("Dark"),
    PUN("Pun"),
    SPOOKY("Spooky"),
    CHRISTMAS("Christmas");

    private final String nombre;

    Categoria(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve la categoría a partir de su nombre.
     *
     * @param nombre Nombre de la categoría
     * @return Categoría
     */
    public static Categoria getCategoria(String nombre) {
        for (Categoria c : Categoria.values()) {
            if (c.getNombre().equals(nombre)) {
                return c;
            }
        }
        return null;
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre de la categoría.
     *
     * @return Nombre de la categoría
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }

}

B) La enumeración TipoChiste contiene los siguientes valores:

public enum TipoChiste {
    SINGLE("single"),
    TWOPART("twopart");
    //...
}
Detalle de implementación de la enumeración TipoChiste
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * Enumeración de tipos de chistes.
 * Pueden ser: single, twopart
 * Atributo: String nombre.
 * Constructor: TipoChiste(String nombre)
 * @see Categoria
 * @see Flag
 * @see Chiste
 *
 */
public enum TipoChiste {
    SINGLE("single"),
    TWOPART("twopart");

    private final String nombre;

    TipoChiste(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve el tipo de chiste a partir de su nombre.
     * @param nombre Nombre del tipo de chiste
     * @return Tipo de chiste
     */
    public static TipoChiste getTipoChiste(String nombre) {
        for (TipoChiste tc : TipoChiste.values()) {
            if (tc.getNombre().equals(nombre)) {
                return tc;
            }
        }
        return null;
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre del tipo de chiste.
     * @return Nombre del tipo de chiste
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }
}

C) La enumeración Flag contiene los siguientes valores:

Flag es una enumeración con los siguientes valores:

```java
public enum Flag {
    EXPLICIT("Explicit"),
    NSFW("NSFW"),
    RELIGION("Religion"),
    POLITICAL("Political"),
    RACIST("Racist"),
    SEXIST("Sexist");
    //...
}
Detalle de implementación de la enumeración Flag
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * Enumeración de banderas de chistes.
 * Pueden ser: NSFW, RELIGION, POLITICAL, RACIST, SEXIST
 * Atributo: String nombre.
 * Constructor: Flag(String nombre)
 * @see Categoria
 * @link <a href="https://v2.jokeapi.dev/flags">https://v2.jokeapi.dev/flags</a>
 */
public enum Flag {
    EXPLICIT("Explicit"),
    NSFW("NSFW"),
    RELIGION("Religion"),
    POLITICAL("Political"),
    RACIST("Racist"),
    SEXIST("Sexist");

    private final String nombre;

    Flag(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve la bandera a partir de su nombre.
     * @param nombre Nombre de la bandera
     * @return Bandera
     */
    public static Flag getFlag(String nombre) {
        // Con expresiones lambda:
        return java.util.Arrays.stream(Flag.values()).filter(f -> f.getNombre().equals(nombre)).findFirst()
                .orElse(null);
/*        // Con un bucle for:
//        for (Flag f : Flag.values()) {
//            if (f.getNombre().equals(nombre)) {
//                return f;
//            }
//        }
//        return null;
        */
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre de la bandera.
     * @return Nombre de la bandera
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }
}

D) Lenguaje es una enumeración con los siguientes valores:

public enum Lenguaje {
    CS("cs"),
    DE("de"),
    EN("en"),
    ES("es"),
    FR("fr"),
    PT("pt");
    //...
}
Detalle de implementación de la enumeración Lenguaje
package com.javhoz.ad.chistes.model;

import java.util.Arrays;

/**
 * Lenguajes admitidos por la API de chistes.
 *  * "jokeLanguages": [
 *  *         "cs",
 *  *         "de",
 *  *         "en",
 *  *         "es",
 *  *         "fr",
 *  *         "pt"
 *  *     ]
 *  Atributo con el nombre del lenguaje del chiste.
 *
 * @see <a href="https://sv443.net/jokeapi/v2/#languages">https://sv443.net/jokeapi/v2/#languages</a>
 */
public enum Lenguaje {
    CS("cs"),
    DE("de"),
    EN("en"),
    ES("es"),
    FR("fr"),
    PT("pt");

    private final String lenguaje;

    /**
     * Constructor de la clase Lenguajes.
     * @param lenguaje Nombre del lenguaje
     */
    Lenguaje(String lenguaje) {
        this.lenguaje = lenguaje;
    }

    /**
     * Devuelve el nombre del lenguaje.
     * @return Nombre del lenguaje
     */
    public String getLenguaje() {
        return lenguaje;
    }

    public static Lenguaje getLenguaje(String lenguaje) {
        // Con expresiones lambda:
        return Arrays.stream(Lenguaje.values()).filter(l -> l.getLenguaje().equals(lenguaje)).findFirst()
                .orElse(null);
        /* // Con un bucle for:
//        for (Lenguaje l : Lenguaje.values()) {
//            if (l.getLenguaje().equals(lenguaje)) {
//                return l;
//            }
//        }
//         return null;
*/

    }
    
    @Override
    public String toString() {
        return lenguaje;
    }
}

Clases

A) La clase Chiste tiene los siguientes atributos:

public class Chiste {
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
    //...
}
Detalle de implementación de la clase Chiste
package com.javhoz.ad.chistes.model;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Updated by javhoz on 16/01/2025.
 * <p>
 * Clase que representa un chiste.
 * Atributos: id de tipo int, categoria de tipo Categoria, idiomade tipo Lenguaje, tipo de TipoChiste,
 *  List<Flag> banderas, String chiste, String respuesta.
 */
public class Chiste {
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;

    /**
     * Constructor de la clase Chiste.
     * @param id Identificador del chiste
     * @param categoria Categoría del chiste
     * @param idioma Idioma del chiste
     * @param tipo Tipo del chiste
     * @param chiste Chiste
     * @param respuesta Respuesta del chiste
     */
    public Chiste(int id, Categoria categoria, String idioma, TipoChiste tipo, String chiste, String respuesta) {
        this.id = id;
        this.categoria = categoria;
        this.tipo = tipo;
        this.chiste = chiste;
        this.respuesta = respuesta;
        this.banderas = new ArrayList<>();
        this.lenguaje = Lenguaje.getLenguaje(idioma);
    }

    /**
     * Constructor por defecto de la clase Chiste.
     *
     */
    public Chiste() {
//        this.id = 0;
        this.categoria = Categoria.ANY;
        this.lenguaje = Lenguaje.EN;
        this.tipo = TipoChiste.SINGLE;
        this.chiste = "";
        this.respuesta = "";
        this.banderas = new ArrayList<>();
    }

    /**
     * Devuelve el identificador del chiste.
     * @return Identificador del chiste
     */
    public int getId() {
        return id;
    }

    /**
     * Establece el identificador del chiste.
     * @param id Identificador del chiste
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * Devuelve la categoría del chiste.
     * @return Categoría del chiste
     */
    public Categoria getCategoria() {
        return categoria;
    }

    public String getCategoriaString() {
        return categoria.getNombre();
    }

    /**
     * Establece la categoría del chiste.
     * @param categoria Categoría del chiste
     */
    public void setCategoria(Categoria categoria) {
        this.categoria = categoria;
    }

    public void setCategoria(String categoria) {
        this.categoria = Categoria.getCategoria(categoria);
    }
    
    public Lenguaje getLenguaje() {
        return lenguaje;
    }

    public String getLenguajeString() {
        return lenguaje.getLenguaje();
    }

    public void setLenguaje(String lenguaje) {
        this.lenguaje = Lenguaje.getLenguaje(lenguaje);
    }

    public void setLenguaje(Lenguaje lenguaje) {
        this.lenguaje = lenguaje;
    }
    
    /**
     * Devuelve el tipo del chiste.
     * @return Tipo del chiste
     */
    public TipoChiste getTipo() {
        return tipo;
    }

    public String getTipoString() {
        return tipo.getNombre();
    }

    /**
     * Establece el tipo del chiste.
     * @param tipo Tipo del chiste
     */
    public void setTipo(TipoChiste tipo) {
        this.tipo = tipo;
    }

    public void setTipo(String tipo) {
        this.tipo = TipoChiste.getTipoChiste(tipo);
    }

    /**
     * Devuelve las banderas del chiste.
     * @return Banderas del chiste
     */
    public List<Flag> getBanderas() {
        return banderas;
    }

    /**
     * Añade una bandera al chiste.
     * @param flag Bandera a añadir
     */
    public void addFlag(Flag flag) {
        banderas.add(flag);
    }

    public boolean removeFlag(Flag bandera) {
        return banderas.remove(bandera);
    }

    /**
     * Si el chiste tiene esa bandera, devuelve true.
     * @param bandera Bandera a comprobar
     * @return  true si el chiste tiene esa bandera, false en caso contrario
     */
    public boolean containsFlag(Flag bandera) {
        return banderas.contains(bandera);
    }
    
    /**
     * Devuelve el chiste como cadena de caracteres.
     * @return Chiste como String
     */
    public String getChiste() {
        return chiste;
    }

    /**
     * Establece el chiste.
     * @param chiste Chiste
     */
    public void setChiste(String chiste) {
        this.chiste = chiste;
    }

    /**
     * Devuelve la respuesta del chiste.
     * @return Respuesta del chiste
     */
    public String getRespuesta() {
        return respuesta;
    }

    /**
     * Establece la respuesta del chiste.
     * @param respuesta Respuesta del chiste
     */
    public void setRespuesta(String respuesta) {
        this.respuesta = respuesta;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Chiste chiste = (Chiste) o;
        return id == chiste.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    /**
     * Sobrescritura del método toString() para que devuelva el chiste.
     * Lo devuelve empleando un StringBuilder y por medio del método forEach() para recorrer la lista de banderas.
     * @return Chiste como String
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Chiste: ").append(chiste).append(System.lineSeparator());
        sb.append("Respuesta: ").append(respuesta).append(System.lineSeparator());
        sb.append("Categoría: ").append(categoria).append(System.lineSeparator());
        sb.append("Idioma: ").append(lenguaje).append(System.lineSeparator());
        sb.append("Tipo: ").append(tipo).append(System.lineSeparator());
        sb.append("Banderas: ");
        banderas.forEach(b -> sb.append(b).append(" "));
        sb.append(System.lineSeparator());
        return sb.toString();
    }

}

B) El adapter ChisteDeserializer:

Detalle de implementación de la clase ChisteDeserializer
package com.javhoz.ad.chistes.model;

import com.google.gson.*;

import java.lang.reflect.Type;

/*
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
"delivery": "Porque C no las trata como objetos.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false
},
"safe": true,
"id": 6,
"lang": "es"
}
 */
public class ChisteDeserializer implements JsonDeserializer<Chiste> {

    @Override
    public Chiste deserialize(JsonElement elemento, Type type,
                              JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {

        // Comprobación si es un objeto:
        if (!elemento.isJsonObject())
            return null;

        // Creo un chiste vacío, al que le daré valor a sus atributos:
        Chiste chiste = new Chiste();
        // Recupero el objeto JSON del chiste
        JsonObject jsonChiste = elemento.getAsJsonObject();
        // Comprobación de que no hay error en la petición:
        if (jsonChiste.get("error") != null && jsonChiste.get("error").getAsBoolean()) {
            return null;
        }
        // Compruebo que cada elemento del objeto existe y lo asigno al objeto Chiste:
        // La comprobación se hace con el método get() de la clase JsonObject que devuelve
        // un JsonElement. Si es null, no existe el elemento.
        if (jsonChiste.get("category") != null) {
            chiste.setCategoria(jsonChiste.get("category").getAsString());
        }
        if (jsonChiste.get("type") != null) {
            chiste.setTipo(jsonChiste.get("type").getAsString());
        }
        // En realidad, dependiendo del tipo de chiste, el setup o el delivery pueden no existir.
        // Por lo que podría hacer comprobando el valor de type, pero lo dejo así para que veáis
        // como se puede hacer con el método get() de la clase JsonObject.
        if (jsonChiste.get("setup") != null) {
            chiste.setChiste(jsonChiste.get("setup").getAsString());
            if (jsonChiste.get("delivery") != null) {
                chiste.setRespuesta(jsonChiste.get("delivery").getAsString());
            }
        } else if (jsonChiste.get("joke") != null) {
            chiste.setChiste(jsonChiste.get("joke").getAsString());
        }

        if (jsonChiste.get("lang") != null) {
            chiste.setLenguaje(jsonChiste.get("lang").getAsString());
        }

        if (jsonChiste.get("id") != null) {
            chiste.setId(jsonChiste.get("id").getAsInt());
        }

        if (jsonChiste.get("flags") != null) {
            JsonObject flags = jsonChiste.get("flags").getAsJsonObject();
            if (flags.get("nsfw").getAsBoolean()) {
                chiste.addFlag(Flag.NSFW);
            }
            if (flags.get("religious").getAsBoolean()) {
                chiste.addFlag(Flag.RELIGION);
            }
            if (flags.get("political").getAsBoolean()) {
                chiste.addFlag(Flag.POLITICAL);
            }
            if (flags.get("racist").getAsBoolean()) {
                chiste.addFlag(Flag.RACIST);
            }
            if (flags.get("sexist").getAsBoolean()) {
                chiste.addFlag(Flag.SEXIST);
            }
            if (flags.get("explicit").getAsBoolean()) {
                chiste.addFlag(Flag.EXPLICIT);
            }
        }
        return chiste;
    }
}

C) La clase ChisteTypeAdapter:

Detalle de implementación de la clase ChisteTypeAdapter
package com.javhoz.ad.chistes.model;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;

/*
Formato de JSON:
{
  "id": 1,
  "category": "Programming",
  "type": "single",
  "joke": "Chuck Norris can write multithreaded applications with a single thread.",
  "flags": {
    "nsfw": false,
    "religious": false,
    "political": false,
    "racist": false,
    "sexist": false
  },
  "lang": "en"
 */

/**
 * Updated by javhoz on 16/01/2025.
 * Clase que adaptará el tipo Chiste para que pueda ser serializado y deserializado por Gson.
 *
 * @see com.google.gson.Gson
 * @see com.google.gson.TypeAdapter
 * @see com.google.gson.GsonBuilder
 * @see com.google.gson.JsonDeserializer
 */
public class ChisteTypeAdapter extends TypeAdapter<Chiste> {

    @Override
    public void write(JsonWriter jsonWriter, Chiste chiste) throws IOException {
        jsonWriter.beginObject();
        jsonWriter.name("id").value(chiste.getId());
        jsonWriter.name("category").value(chiste.getCategoriaString());
        jsonWriter.name("type").value(chiste.getTipoString());
        if (chiste.getTipo() == TipoChiste.SINGLE) {
            jsonWriter.name("joke").value(chiste.getChiste());
        } else {
            jsonWriter.name("setup").value(chiste.getChiste());
            jsonWriter.name("delivery").value(chiste.getRespuesta());
        }
        jsonWriter.name("flags");
        jsonWriter.beginObject();
        // Recorremos todas las banderas y asignamos el valor verdadero o falso si el chiste la contiene o no, respectivamente.
        // Puede hacerse por medio del método containsFlag() de la clase Chiste o recoger las banderas
        // del chiste e invocar el método contains() de la clase List.
        for (Flag flag : Flag.values()) {
            jsonWriter.name(flag.getNombre().toLowerCase()).value(chiste.containsFlag(flag));
        }
        jsonWriter.endObject();
        jsonWriter.name("lang").value(chiste.getLenguajeString());
        jsonWriter.endObject();

    }

    /**
     * Método que deserializa un objeto Chiste a partir de un JsonReader.
     *
     * @param reader JsonReader que contiene el objeto Chiste
     * @return Objeto Chiste
     * @throws IOException Si hay un error de E/S
     * @see com.google.gson.stream.JsonReader
     * @see com.google.gson.stream.JsonToken
     */
    @Override
    public Chiste read(JsonReader reader) throws IOException {
        if(reader.peek()== JsonToken.NULL || reader.peek()!= JsonToken.BEGIN_OBJECT){
            // reader.nextNull();
            return null;
        }
        reader.beginObject();
        Chiste chiste = new Chiste();
        while (reader.peek() != JsonToken.END_OBJECT) {
            String name = reader.nextName();
            switch (name) {
                case "id" -> chiste.setId(reader.nextInt());
                case "category" -> chiste.setCategoria(Categoria.getCategoria(reader.nextString()));
                case "type" -> chiste.setTipo(TipoChiste.getTipoChiste(reader.nextString()));
                case "joke", "setup" -> chiste.setChiste(reader.nextString());
                case "delivery" -> chiste.setRespuesta(reader.nextString());
                case "flags" -> // Para hacerlo más modular he puesto el código en un método aparte.
                        readFlags(reader, chiste);
                case "lang" -> chiste.setLenguaje(reader.nextString());
                default -> reader.skipValue();
            }
        }
        reader.endObject();

        return chiste;
    }

    private void readFlags(JsonReader reader, Chiste chiste) throws IOException {
        reader.beginObject();
        while (reader.peek() != JsonToken.END_OBJECT) {
            String flagName = reader.nextName();
            switch (flagName) {
                case "nsfw" -> {
                    if (reader.nextBoolean()) chiste.addFlag(Flag.NSFW);
                }
                case "religious" -> {
                    if (reader.nextBoolean()) chiste.addFlag(Flag.RELIGION);
                }
                case "political" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.POLITICAL);
                }
                case "racist" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.RACIST);
                }
                case "sexist" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.SEXIST);
                }
                case "explicit" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.EXPLICIT);
                }
                default -> reader.skipValue();
            }
        }
        reader.endObject();
    }
}

D) La interface IChisteDAO y clase ChisteDAO se usa para obtener los chistes de la API:

Detalle de implementación de la interfaz IChisteDAO
package com.javhoz.ad.chistes.model;

import java.io.Writer;

public interface IChisteDAO {

    String getRandomJokeAsString();
    String getJokeAsString(String categoria, String[] tipo, String[] banderas);
    String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma);

    Chiste getRandomJoke();
    Chiste getJoke(String categoria, String[] tipo, String[] banderas);
    Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma);

    Chiste getJokeById(int id);


    void saveJokeAsJson(Chiste chiste, Writer writer);

}

Podrías realizar mejoras en el código, como la gestión de excepciones, la comprobación de valores nulos, la simplificación de código, etc.

Detalle de implementación de la clase ChisteDAO
package com.javhoz.ad.chistes.model;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Objects;

/**
 * Created by Pepe Calo on 07/11/2023
 * Implementación de la interfaz IChisteDAO que consulta un chiste en un archivo Json
 * mediante la librería Gson.
 * La API de chistes utilizada es:
 * <a href="https://v2.jokeapi.dev/joke/">...</a>
 *
 * @see IChisteDAO
 * @see Chiste
 * @see Gson
 * @see GsonBuilder
 * @see com.google.gson.JsonObject
 * @see com.google.gson.JsonParser
 */
public class ChisteDAO implements IChisteDAO {
    private final Gson gson;

    // https://v2.jokeapi.dev/joke/Programming,Miscellaneous?blacklistFlags=nsfw,religious

    private static final String BASE_URL = "https://v2.jokeapi.dev/joke/";
    private static final String ENDPOINT = "?format=json";
    private static final int NO_ID = 0;

    private static final String SINGLE = "single";

    /**
     * Constructor de la clase ChisteDAO.
     * Si deseas emplear las clases ChisteSerializer y ChisteDeserializer, debes comentar la línea con ChisteTypeAdapter
     * y no comentar las de los otros dos adaptadores.
     */
    public ChisteDAO() {
        gson = new GsonBuilder().setPrettyPrinting()
//                .registerTypeAdapter(Chiste.class, new ChisteDeserializer())
//                .registerTypeAdapter(Chiste.class, new ChisteSerializer())
                .registerTypeAdapter(Chiste.class, new ChisteTypeAdapter())
                .create();
    }


    private String getURL(String categoria, String[] tipo, String[] banderas, String idioma, int id) {
        String url = BASE_URL + categoria + ENDPOINT;
        if (tipo != null && tipo.length > 0) {
            // Concateno los elementos no nulos media stream de un array de String. En el caso de que no haya ninguno, devuelvo un Optional vacío.
            String tipos = Arrays.stream(tipo).filter(Objects::nonNull).reduce((s, s2) -> s + "," + s2).orElse(null);
            if(tipos!=null && !tipos.isEmpty()){
                url += "&type=" + tipos;
            }
        }
        if (banderas != null && banderas.length > 0) {
            String flags = Arrays.stream(banderas).filter(Objects::nonNull).reduce((s, s2) -> s + "," + s2).orElse(null);
            if(flags!=null && !flags.isEmpty()){
                url += "&blacklistFlags=" + flags;
            }
        }
        if (idioma != null && !idioma.isEmpty()) {
            url += "&lang=" + idioma;
        }
        if (id > 0) {
            url += "&idRange=" + id;
        }
        System.out.println("url = " + url);
        return url;
    }

    private Chiste getJoke(String url) {
        try (BufferedReader is = new BufferedReader(new InputStreamReader(new URI(url).toURL().openStream()))) {
            return gson.fromJson(is, Chiste.class);
        } catch (MalformedURLException e) {
            System.err.println("Error en la URL: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Erro E/S: " + e.getMessage());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    private String getJokeAsString(String url) {
        Chiste chiste = getJoke(url);
        return (chiste!=null) ? chiste.getChiste() + System.lineSeparator() + chiste.getRespuesta() : "";
    }


    @Override
    public String getJokeAsString(String categoria, String[] tipo, String[] banderas) {
        return getJokeAsString(getURL(categoria, tipo, banderas, null, NO_ID));
    }


    @Override
    public Chiste getJoke(String categoria, String[] tipo, String[] banderas) {
        return getJoke(getURL(categoria, tipo, banderas, null, NO_ID));
    }

    @Override
    public String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma) {
        return getJokeAsString(getURL(categoria, tipo, banderas, idioma, NO_ID));
    }

    @Override
    public Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma) {
        return getJoke(getURL(categoria, tipo, banderas, idioma, NO_ID));
    }

    @Override
    public Chiste getJokeById(int id) {
        return getJoke(getURL("Any", null, null, null, id));
    }

    @Override
    public void saveJokeAsJson(Chiste chiste, Writer writer) {
        gson.toJson(chiste, writer);
    }

    @Override
    public String getRandomJokeAsString() {
        System.out.println(BASE_URL + "Any");
        return getJokeAsString(BASE_URL + "Any");
    }

    @Override
    public Chiste getRandomJoke() {
        return getJoke(BASE_URL + "Any");
    }
    
}

Ejercicio

Crear una base de datos con JPA y Hibernate para la aplicación JokeAPI y transfiere todos los datos de JSON a la base de datos.

Añade las dependencias necesarias y el fichero de configuración persistence.xml en el directorio META-INF de src/main/resources:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="chistesH2" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>-->
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:RutaABaseDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/>
            <property name="jakarta.persistence.jdbc.user" value=""/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <!-- Automáticamente, genera el esquema de la base de datos -->
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>

            <!-- Muestra por pantalla las sentencias SQL -->
            <property name="hibernate.show_sql" value="false"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.highlight_sql" value="true"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        </properties>
    </persistence-unit>
</persistence>

Para ello, crea las siguientes clases:

A) ChisteJpaManager que empleando el patrón Singleton, gestione la creación de la factoría de entidades y el EntityManager.

Solución de ChisteJpaManager
package com.javhoz.ad.chistes.model;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;
import java.util.Map;

import static com.javhoz.ad.chistes.model.ChisteLogger.LOG;

public class ChisteJpaManager {

    public static final String BIBLIOTECA_H2 = "chistesH2";
    public static final String BIBLIOTECA_POSTGRES = "chistesPostgres";


    private static final Map<String, EntityManagerFactory> instancies = new HashMap<>();

    private ChisteJpaManager() {
    }

    private static boolean isEntityManagerFactoryClosed(String unidadPersistencia) {
        return !instancies.containsKey(unidadPersistencia) || instancies.get(unidadPersistencia) == null ||
                !instancies.get(unidadPersistencia).isOpen();
    }

    public static EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        if (isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (ChisteJpaManager.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    try {
                        instancies.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                    } catch (Exception e) {
                        LOG.error("Erro ó crear a unidade de persistencia " + unidadPersistencia +
                                ": " + e.getMessage());
                    }
                }
            }
        }
        return instancies.get(unidadPersistencia);
    }


    public static EntityManager getEntityManager(String persistenceUnitName) {
        return getEntityManagerFactory(persistenceUnitName).createEntityManager();
    }


    public static void close(String persistenceUnitName) {
        if (instancies.containsKey(persistenceUnitName)) {
            instancies.get(persistenceUnitName).close();
            instancies.remove(persistenceUnitName);
        }
    }

}

B) Chiste que emplea JPA para mapear la clase Chiste con la tabla Chiste de la base de datos.

Solución de Chiste
package com.javhoz.ad.chistes.model;

import jakarta.persistence.*;
import java.util.ArrayList;

@Entity
public class Chiste implements java.io.Serializable {
    @Id
    @Column(name = "idChiste")
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    // Como se trata de una relación muchos a muchos, se emplea la anotación @ElementCollection
    // H2 admite el tipo de dato Array de enteros (TINYINT ARRAY), prueba a no poner la anotación @ElementCollection ni @CollectionTable
    @ElementCollection // Para que se cree una tabla intermedia
    @Enumerated(EnumType.STRING)
    @CollectionTable(name = "FlagsChiste", joinColumns = @JoinColumn(name = "idChiste"))
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
//...
}

C) ChisteDownloader que descarga los chistes de la API y los guarda en la base de datos.

Ten el cuenta que ChisteDownloader es un Singleton y que se puede configurar el número de chistes a descargar, además de un tiempo de espera entre chiste y chiste (la API sólo permite 120 peticiones por minuto).

Por ello, haz que sea un hilo que se ejecute cada cierto tiempo ( implements Runnable ) y tenga los siguientes atributos:

  • tiempoEspera que es el tiempo de espera entre chiste y chiste.
  • instance que es la instancia de ChisteDownloader.
  • MAX_CHISTES que es el número máximo de chistes a descargar.
  • chisteDAO que es el DAO de Chiste.
  • numeroChistes que es el número de chistes a descargar (si no se indica debe ser MAX_CHISTES).
Solución de ChisteDownloader
package com.javhoz.ad.chistes;

import com.javhoz.ad.chistes.model.Chiste;
import com.javhoz.ad.chistes.model.ChisteDAO;
import com.javhoz.ad.chistes.model.ChisteJpaManager;

import static java.lang.Thread.sleep;

public class ChisteDownloader implements Runnable {

    private static final long tiempoEspera = 550;
    private static ChisteDownloader instance;
    private static final int MAX_CHISTES = 200;

    private ChisteDAO chisteDAO;
    private int numeroChistes = MAX_CHISTES;

    private ChisteDownloader() {
        chisteDAO = new ChisteDAO();
    }

    public static ChisteDownloader getInstance() {
        if (instance == null) {
            synchronized (ChisteDownloader.class) {
                if (instance == null) {
                    instance = new ChisteDownloader();
                }
            }
        }
        return instance;
    }


    public void setNumeroChistes(int numeroChistes) {
        this.numeroChistes = numeroChistes;
    }

    @Override
    public void run() {
        chisteDAO = new ChisteDAO();
        var emf = ChisteJpaManager.getEntityManagerFactory(ChisteJpaManager.BIBLIOTECA_H2);
        var em = emf.createEntityManager();

        for (int i = 0; i < numeroChistes; i++) {
            Chiste chiste = chisteDAO.getJokeById(i);
            if (chiste != null) {
                try {
                    em.getTransaction().begin();
                    em.persist(chiste);
                    em.getTransaction().commit();
                } catch (Exception e) {
                    em.getTransaction().rollback();
                }
                System.out.print("*");
                try {
                    sleep(tiempoEspera);
                } catch (InterruptedException e) {
                    System.out.println("Error en el hilo");
                }
            }

        }

    }
}

D) Main que descarga los chistes y los guarda en la base de datos.

Solución de Main
    public static void main(String[] args) {

        ChisteDownloader chisteDownloader = ChisteDownloader.getInstance();
        chisteDownloader.setNumeroChistes(300);
        Thread thread = new Thread(chisteDownloader);
        thread.start();
    }
Última actualización: 23.09.2025

05. Mapeo de entidades.

1. Mapeo Objeto-Relacional

El componente de mapeo objeto-relacional (ORM) incluye:

  • Correspondencia del estado del objeto con las columnas de la base de datos.
  • Cómo enviar consultas entre los objetos.

En este apartado veremos cómo mapear entidades y atributos con la base da datos y generar automáticamente identificadores de entidad.

2. Anotaciones de Persistencia

  • Las especificaciones de Jakarta Persistence (y de Enterprise Beans) emplea principalmente anotaciones.
  • Las anotaciones pueden aplicarse a clases, métodos y atributos.
  • La anotación debe colocarse principio a la definición de código del artefacto que se está anotando: bien en la misma línea justo antes de la clase, método o atributo o en la línea superior.
Consejo

La elección se basa completamente en las preferencias de la persona que aplica las anotaciones, y creo que tiene sentido hacer una cosa en algunos casos y la otra en otros casos. Depende de cuán extensa sea la anotación y cuál sea el formato más legible.

Las anotaciones de Jakarta Persistence fueron diseñadas para ser legibles, fáciles de especificar y lo suficientemente flexibles como para permitir diferentes combinaciones de metadatos. La mayoría de las anotaciones se especifican como hermanas en lugar de estar anidadas entre sí, lo que significa que múltiples anotaciones pueden anotar la misma clase, atributo o propiedad en lugar de tener anotaciones incrustadas dentro de otras anotaciones.

Las anotaciones de mapeo se pueden clasificar en dos categorías:

  • Anotaciones lógicas: describen el modelo de entidad desde una perspectiva de modelado de objetos. Están fuertemente vinculadas al modelo de dominio y son el tipo de metadatos que podría querer especificar en UML o cualquier otro lenguaje o marco de modelado de objetos Ejemplos: @Entity, @Id, @ManyToOne, @OneToMany, @ManyToMany, @OneToOne.

  • Anotaciones físicas: se relacionan con el modelo de datos concreto de la base de datos. Tratan con tablas, columnas, restricciones y otros artefactos en base de datos de los que el modelo de objetos podría no estar al tanto de otra manera. Ejemplos: @Table, @Column, @JoinColumn, @JoinTable.

Existen equivalentes XML para todas las anotaciones de mapeo lo que permite utilizar el enfoque que mejor se adapte a las necesidades de desarrollo. Nosotros nos centraremos en anotaciones, que es la forma más común de especificar metadatos en aplicaciones modernas.

Nota

Consejo: Las anotaciones de mapeo de JPA se pueden aplicar a atributos o métodos. Si se aplican a un atributo, el proveedor de persistencia accederá al atributo directamente. Si se aplican a un método, el proveedor de persistencia accederá al atributo a través del método getter y setter. Lo veremos ahora.

1. Modo de acceso a una Entidad

La forma en que se accede al estado en la entidad desde el proveedor de persistencia se llama modo de acceso.

El mecanismo que se usa para designar el estado persistente es el mismo que el modo de acceso que el proveedor utiliza para acceder a ese estado, y hay dos modos de acceso: acceso por atributo (atributos de la entidad) y acceso por propiedad (métodos getter y setter de la entidad).

  • Acceso por atributo: a partir de los atributos/atributos de la entidad utilizando reflexión (Java reflection) (se precisa un @Id sobre el atributo).

  • Acceso por propiedad: las anotaciones se colocan en los métodos getter de las propiedades, esos métodos getter y setter serán invocados por el proveedor para acceder y establecer el estado. En este caso se indica la anotación @Id en el método getter.

1.2. Acceso por atributo

Anotar los atributos de la entidad hará que el proveedor use el acceso por atributo para obtener y establecer el estado de la entidad. Los métodos getter y setter pueden estar presentes o no, pero si están presentes, el proveedor los ignora.

Todos los atributos deben declararse como protected, de paquete (sin modificador) o private. Se prohíben los atributos public.

El ejemplo de entidad Employee mapeada usando el acceso por atributo:

  • La anotación @Id indica que id es el identificador persistente o clave primaria de la entidad y que se debe asumir el acceso por atributo.
  • Los atributos name y salary se configuran por defecto como persistentes y se mapean a columnas del mismo nombre.
@Entity
public class Employee {
 @Id
 private Long id;
 private String name;
 private long salary;
 
 public Long getId() { return id; }
 public void setId(Long id) { this.id = id; }
 public String getName() { return name; }
 public void setName(String name) { this.name = name; }
 public long getSalary() { return salary; }
 public void setSalary(long salary) { this.salary = salary; }
}

1.2. Acceso por propiedad

Cuando se utiliza el modo de acceso por propiedad debe haber métodos getter y setter para las propiedades persistentes.

  • El tipo de propiedad se determina por el tipo devuelto del método getter y debe ser el mismo que el tipo del único parámetro pasado al método setter.
  • Ambos métodos deben tener visibilidad public o protected.
  • Las anotaciones de mapeo para una propiedad deben estar en el método getter.

Ejemplo de la clase Employee tiene una anotación @Id en el método getId(), por lo que el proveedor utilizará el acceso por propiedad para obtener y establecer el estado de la entidad. Las propiedades name y salary se harán persistentes gracias a los métodos getter y setter y se mapearán a las columnas NAME y SALARY, respectivamente.

Observa que la propiedad salary está respaldada por el atributo wage, que no comparte el mismo nombre. Esto pasa desapercibido para el proveedor porque al especificar el acceso por propiedad, le estamos diciendo al proveedor que ignore los atributos de la entidad y utilice solo los métodos getter y setter para la nomenclatura.

@Entity
public class Employee {
    
 private long id;
 private String name;
 private long wage;
 
 @Id
 public long getId() { return id; }
 public void setId(long id) { this.id = id; }
 public String getName() { return name; }
 public void setName(String name) { this.name = name; }
 public long getSalary() { return wage; }
 public void setSalary(long salary) { this.wage = salary; }
}

1.3. Acceso Mixto: @Access(AccessType.FIELD|AccessType.PROPERTY))

Por lo general se accede a los datos a través del acceso por atributo, pero posible combinar el acceso por atributo con el acceso por propiedad dentro de la misma jerarquía de entidades o dentro de la misma entidad. Puede ser útil cuando se agrega una subclase de entidad a una jerarquía existente que utiliza un tipo de acceso diferente.

Agregar una anotación @Access con un modo de acceso hace que el tipo de acceso predeterminado se anule para esa subclase de entidad.

@Access también es útil cuando es necesario realizar una simple transformación de los datos al leer o escribir en la base de datos.

Por ejemplo, la entidad Employee que tiene un modo de acceso predeterminado de AccessType.FIELD, pero la columna de la base de datos almacena el código de área como parte del número de teléfono, y solo queremos almacenar el código de área en el atributo phoneNum de la entidad si no es un número local. Podemos agregar una propiedad persistente que realice la transformación correspondiente en lecturas y escrituras.

  1. Se debe hacer es marcar explícitamente el modo de acceso predeterminado para la clase mediante la anotación @Access e indicar el tipo de acceso. A menos que se haga esto, será indefinido si ambos atributos y propiedades están anotados:
@Entity
@Access(AccessType.FIELD)
public class Employee {
    // ... 
}
  1. Se anota el atributo o propiedad adicional con la anotación @Access, pero esta vez especificando el tipo de acceso opuesto al especificado a nivel de clase. No es redundante especificar el tipo de acceso de AccessType.PROPERTY en una propiedad persistente porque es obvio al verlo que es una propiedad, pero al hacerlo se indica que es una excepción al caso predeterminado:
@Access(AccessType.PROPERTY)
@Column(name="PHONE")
protected String getPhoneNumberForDb() {
    // ... 
}
  1. El atributo o propiedad correspondiente al que se está haciendo persistente debe marcarse como *transient* para que las reglas de acceso predeterminadas no provoquen que el mismo estado se persista dos veces. El atributo en el cual se está almacenando el estado de la propiedad persistente en la entidad debe estar anotado con @Transient:
@Transient
private String phoneNum; // no persiste este atributo, pues se persiste el atributo por la propiedad getPhoneNumberForDb()

Ejemplo completo de la clase Employee con un atributo phoneNum que se mapea a la columna PHONE de la base de datos, pero que realiza una transformación simple en la lectura y escritura:

@Entity
@Access(AccessType.FIELD)
public class Employee {
    
     public static final String LOCAL_AREA_CODE = "613";
     @Id private long id;
     @Transient private String phoneNum;
    // ...
    
     public long getId() { return id; }
     public void setId(long id) { this.id = id; }
     public String getPhoneNumber() { return phoneNum; }
     public void setPhoneNumber(String num) {
         this.phoneNum = num;
     }
    
     @Access(AccessType.PROPERTY)
     @Column(name="PHONE") // Si no se indica la columna, se mapearía a PhoneNumberForDb
     protected String getPhoneNumberForDb() { 
        if (phoneNum.length() == 10)
            return phoneNum;
         else
         return LOCAL_AREA_CODE + phoneNum;
     }
     
     protected void setPhoneNumberForDb(String num) {
         if (num.startsWith(LOCAL_AREA_CODE))
            phoneNum = num.substring(3);
         else
            phoneNum = num;
     }
    // ...
}
Ejercicio 05.01. Acceso combinado a la entidad Chiste.

Mofifica la entidad Chiste para que guarde el chiste y la respuesta en un solo campo en la base de datos, pero que se muestren por separado en la aplicación.

@Entity
public class Chiste {
    @Id
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
    // ...
}

2. Mapeo a una Tabla concreta: @Table

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/table

Para mapear entidad a una tabla de la base de datos una entidad sólo se necesitan las anotaciones @Entity y @Id.

El nombre de la tabla predeterminado es el nombre no calificado de la clase de entidad.

Para cambiar el nombre predeterminado de la tabla se anota la clase de entidad con la anotación @Table e incluyendo el nombre de la tabla mediante el elemento name. Por ejemplo:

@Entity
@Table(name="EMP")
public class Employee {
    // ... 
}
Consejo

Consejo Los nombres predeterminados no se especifican en mayúsculas o minúsculas. La mayoría de las bases de datos no distinguen entre mayúsculas y minúsculas, por lo que generalmente no importará si un proveedor usa el caso del nombre de la entidad o lo convierte a mayúsculas.

"_In MySQL text columns are case insensitive by default, while in H2 they are case
sensitive. However H2 supports case insensitive columns as well. To create the tables
with case insensitive texts, append IGNORECASE=TRUE to the database URL (example:
jdbc:h2:~/test;IGNORECASE=TRUE)."_

Esquemas:

La anotación @Table proporciona la capacidad también de especificar un esquema o catálogo de la base de datos. El nombre del esquema se usa comúnmente para diferenciar un conjunto de tablas de otro y se indica mediante el uso del elemento schema. Por ejemplo: la entidad Employee que se asigna a la tabla EMP en el esquema HR.

@Entity
@Table(name="EMP", schema="HR")
public class Employee { ... }

El nombre del esquema se antepondrá al nombre de la tabla cuando el proveedor de persistencia vaya a la base de datos para acceder a la tabla (HR.EMP en el ejemplo).

Consejo

Algunos proveedores pueden permitir que el esquema se incluya en el elemento name de la tabla sin tener que especificar el elemento schema, como en @Table(name="HR.EMP"), pero no es un estándar.

Catálogos:

Algunas bases de datos admiten la noción de un catálogo. Para estas bases de datos, se puede especificar el elemento catalog de la anotación @Table. Por ejemplo, para la tabla EMP.

@Entity
@Table(name="EMP", catalog="HR")
public class Employee {
    // ... 
}

2.1. Nombres sensibles a mayúsculas de tablas y columnas

Los nombres de las tablas y columnas como identificadores en mayúsculas ayudan a diferenciarlos de los identificadores en Java y es el estándar SQL establece que los identificadores de base de datos no delimitados no sean sensibles a mayúsculas y la mayoría tiende a mostrarlos en mayúsculas.

@Table y @Column la cadena de identificador se pasa al controlador JDBC exactamente como se especifica o establece por defecto. Por ejemplo, cuando no se especifica un nombre de tabla para la entidad Autor, entonces el nombre de la tabla asumido y utilizado por el proveedor será Autor, que por definición SQL no es diferente de AUTOR.

Las siguientes anotaciones deberían ser equivalentes, ya que se refieren a la misma tabla en una base de datos compatible con el estándar SQL:

@Table(name="autor")
@Table(name="Autor")
@Table(name="AUTOR")

Aunque no es común ni una buena práctica, en teoría, una base de datos podría tener una tabla AUTOR y otra Autor. Estas necesitarían ser envueltas en comillas dobles para distinguirlas, que deben ser escapadas, alrededor del identificador. El mecanismo de escape es la barra invertida (el carácter ):

@Table(name="\"Autor\"")
@Table(name="\"AUTOR\"") // Son tablas diferentes

3. Mapeo de Tipos Simples

Los tipos simples de Java se asignan de manera inmediata en campos o propiedades de una entidad. Incluyen:

  • Tipos primitivos de Java: byte, int, short, long, boolean, char, float y double.
  • Clases envolventes de tipos primitivos de Java: Byte, Integer, Short, Long, Boolean, Character, Float y Double
  • Tipos de arrays de byte y carácter: byte[], Byte[], char[] y Character[]
  • Tipos numéricos grandes: java.math.BigInteger y java.math.BigDecimal
  • Cadenas: java.lang.String
  • Tipos temporales de Java: java.util.Date y java.util.Calendar, además de todos los subtipos y las Java 8 java.time API:
    • java.time.LocalDate
    • java.time.LocalTime
    • java.time.LocalDateTime
    • java.time.OffsetTime
    • java.time.OffsetDateTime
    • Para tipos como java.time.Instant, se necesita un AttributeConverter, que veremos más adelante.
  • Tipos temporales JDBC: java.sql.Date, java.sql.Time y java.sql.Timestamp
  • Tipos enumerados: cualquier tipo enumerado definido por el sistema o el usuario
  • Objetos serializables: cualquier tipo serializable definido por el sistema o el usuario.

Si el tipo de la capa JDBC no se puede convertir al tipo de Java del campo o propiedad, normalmente se lanzará una excepción, aunque no está garantizado.

Consejo

Cuando el tipo persistente no coincide con el tipo JDBC, algunos proveedores pueden optar por tomar medidas propietarias o hacer una suposición para convertir entre los dos. En otros casos, el controlador JDBC podría realizar la conversión por sí mismo.

Opcionalmente, se puede colocar una anotación @Basic en un campo o propiedad para marcarlo explícitamente como persistente. Esta anotación es principalmente con fines de documentación y no es necesaria para que el campo o propiedad sea persistente.

4. Mapeo de columnas: @Column

La anotación @Basic (o el mapeo básico asumido en su ausencia) puede considerarse como una indicación lógica de que un atributo dado es persistente.

La anotación física que acompaña al mapeo básico es la anotación @Column:

@Column

Con @Column en el atributo indica características específicas de la columna física de la base de datos. El nombre de la columna y los metadatos de asignación física pueden estar en un archivo XML separado.

Elementos de la anotación @Column:

  • name: nombre de la columna de la base de datos. String predeterminado es el nombre del atributo o propiedad.
  • length: longitud de la columna de la base de datos. Solo se aplica si el tipo de columna es una cadena o un tipo de array de caracteres. Por defecto 255.
  • unique: si es una clave primaria (valor único). Booleano con un valor predeterminado de false.
  • nullable: si puede ser nulo. Booleano con un valor predeterminado de true.
  • insertable: si el valor de la columna se incluye en las declaraciones de SQL INSERT generadas. Booleano con un valor predeterminado de true.
  • updatable: si el valor de la columna se incluye en las declaraciones de SQL UPDATE generadas por el proveedor. Este es un atributo booleano con un valor predeterminado de true.
  • precision y scale: se aplican a los tipos numéricos y se utilizan para especificar la precisión y la escala de la columna de la base de datos. Si se omite, se utilizarán los valores predeterminados 0.
  • table: El nombre de la tabla de la base de datos que contiene la columna. Este nombre se refiere a la tabla que contiene la columna, que puede ser la tabla de la entidad o una tabla secundaria. Esta anotación es útil para especificar una columna que se mapea a una tabla secundaria.
  • columnDefinition: definición de columna SQL, que es una cadena que se pasará directamente al DDL de la base de datos. Esta característica puede hacer que la aplicación sea menos portátil. Si se omite, se utilizará la definición de columna predeterminada del proveedor de persistencia.

El principal elemento que es relevante es el elemento name, que es simplemente una cadena que especifica el nombre de la columna a la que se ha asignado el atributo.

@Entity
public class Employee {
    @Id
    @Column(name="EMP_ID")
    private long id;
    private String name;
    @Column(name="SAL")
    private long salary;
    @Column(name="COMM")
    private String comments;
    // ...
}

5. Carga perezosa (Lazy Fetching): @Basic(fetch=FetchType.LAZY)

A veces, alguna parte de la entidad se accede pocas veces (imagen, etc.). En estas situaciones, se puede optimizar el rendimiento al recuperar sólo los datos que se espera que se accedan con frecuencia. Es lo que se denomina: carga perezosa, carga diferida, carga lenta, carga bajo pedido, lectura justo a tiempo, indirección y otros.

En este caso, los datos del objeto no se leen inicialmente desde la base de datos, sino que se recuperarán solo cuando se hagan referencia o se lean.

Se especifica con el elemento fetch de la anotación @Basic, que se corresponde con un valor de la enumeración FetchType:

  • EAGER (por defecto): carga ansiosa.
  • LAZY: el proveedor puede posponer la carga del estado para ese atributo hasta que se haga referencia.

Casi nunca es una buena idea cargar perezosamente los tipos simples. Las únicas veces en las que debería considerarse la carga perezosa de un mapeo básico son cuando hay muchas columnas en una tabla (por ejemplo, docenas o cientos) o cuando las columnas son grandes (por ejemplo, cadenas de caracteres o cadenas de bytes muy grandes).

@Entity
public class Employee {
    // ...
    @Basic(fetch=FetchType.LAZY)
    @Column(name="COMM")
    private String comments;
    // ...
}

La aplicación no tiene que hacer nada especial para obtenerlo. Al acceder al campo de comentarios, se leerá y completará automáticamente por el proveedor si aún no se había cargado.

Consejo

La directiva LAZY solo pretende ser una sugerencia para el proveedor de persistencia para ayudar a la aplicación a lograr un mejor rendimiento. No se requiere que el proveedor respete la solicitud porque el comportamiento de la entidad no se ve comprometido si el proveedor procede y carga el atributo. La situación contraria no es cierta, ya que especificar que un atributo se cargue ansiosamente podría ser fundamental para poder acceder al estado de la entidad una vez que la entidad se ha desvinculado del contexto de persistencia.

6. Objetos Grandes (LOBs): @Lob

Un LOB es un campo de caracteres o bytes que puede ser muy grande (hasta el rango de gigabytes). Típicamente, CLOB se utiliza para almacenar texto y BLOB para almacenar datos binarios. Los LOB se almacenan en la base de datos, pero se accede a ellos de manera diferente a los tipos simples.

La anotación @Lob se puede usar para los LOB y puede aparecer junto con la anotación @Basic, o puede aparecer cuando @Basic está ausente y se asume implícitamente en el mapeo.

Dado que la anotación @Lob realmente sólo califica el mapeo básico, también puede ir acompañada de una anotación @Column.

Existen (básicamente) dos tipos de LOB en las BD:

  • CLOB contiene una secuencia de caracteres grande. Los tipos de datos Java son char[], Character[] y objetos String.
  • BLOB puede almacenar una secuencia de bytes grande. Los tipos de Java asignados a columnas BLOB son byte[], Byte[] y tipos Serializable.

Un ejemplo, en el que se marca LAZY, algo útil en los LOB poco empleados:

@Entity
public class Employee {
    @Id
    private long id;
    @Basic(fetch=FetchType.LAZY)
    @Lob
    @Column(name="PIC")
    private byte[] picture;
    // ...
}
Ejercicio 05.02. CLOB y BLOB de una entidad Documento

Crea una entidad Documento que tenga un campo de texto grande (CLOB) para el contenido del documento y un campo de bytes grande (BLOB) para la imagen del documento. Haz pruebas con tres gestores de bases de datos: H2, SQLite y PostgreSQL y comprueba el resultado creando la tabla en cada uno de ellos, con y sin declaración de tipo de LOB.

    
@Entity
public class Documento {
    @Id
    private long id;
    @Lob
    private String contenido;
    @Lob
    private byte[] imagen;
    // ...
}
Consejo

Consejo: Los LOB son útiles para almacenar datos grandes, pero no se deben abusar de ellos. Los LOB pueden ser ineficientes para recuperar y almacenar. Siempre que sea posible, se deben evitar los LOB. Si se necesita almacenar datos grandes, se debe considerar el uso de un sistema de archivos o un sistema de almacenamiento de objetos.

7. Tipos Enumerados (enum): @Enumerated

Los valores de un tipo enumerado en Java tienen una asignación ordinal implícita que se determina por el orden en que se declararon.

El ordinal se usa de modo predeterminado para representar y almacenar los valores del tipo enumerado en la base de datos.

El proveedor asumirá que la columna de la base de datos es de tipo entero.

Por ejemplo, el tipo enumerado EmployeeType:

public enum EmployeeType {
    FULL_TIME_EMPLOYEE,
    PART_TIME_EMPLOYEE,
    CONTRACT_EMPLOYEE
}

Los ordinales en tiempo de compilación serían 0 para FULL_TIME_EMPLOYEE, 1 para PART_TIME_EMPLOYEE y 2 para CONTRACT_EMPLOYEE.

@Entity
public class Employee {
    @Id
    private long id;
    private EmployeeType type;
    // ...
}

7.1. Mapeo enumeraciones como cadenas

EmployeeType, en ejemplo anterior, el atributo type se asignará a una columna TYPE de tipo entero.

Si se cambia el tipo (el orden) hay una inconsistencia y problemas.

En este ejemplo, si la política de beneficios de la empresa cambia y comenzamos a dar beneficios adicionales a los empleados a tiempo parcial que trabajan más de 20 horas por semana, querríamos diferenciar entre los dos tipos de empleados a tiempo parcial. Al agregar un valor PART_TIME_BENEFITS_EMPLOYEE después de PART_TIME_EMPLOYEE, estaríamos provocando una nueva asignación de ordinal, donde nuestro nuevo valor recibiría el ordinal 2 y CONTRACT_EMPLOYEE obtendría 3. Esto tendría el efecto de hacer que todos los empleados contratados previamente como empleados a tiempo parcial se conviertan repentinamente en empleados a tiempo parcial con beneficios, claramente no el resultado que esperábamos.

Una solución es almacenar el nombre de la enumeración como una cadena en lugar de almacenar el ordinal. Para ello existe la anotación @Enumerated:

@Enumerated

Para modificar cómo guardar los enumerados se puede realizar con la anotación @Enumerated en el atributo y especificando un valor de EnumType.STRING (la otra posibilidad es EnumType.ORDINAL):

EnumType

La anotación @Enumerated permite especificar un EnumType, que a su vez es un tipo enumerado que define el valor de value de EnumType.ORDINAL y EnumType.STRING.

El valor predeterminado de @Enumerated es ORDINAL, especificar @Enumerated(ORDINAL) solo es útil cuando se desea hacer explícito este mapeo.

Por ejemplo:

@Entity
public class Employee {
    @Id
    private long id;
    
    @Enumerated(EnumType.STRING)
    private EmployeeType type;
    
    // ...
}

El uso de cadenas resuelve el problema de insertar valores adicionales en medio del tipo enumerado, pero dejará los datos vulnerables a cambios en los nombres de los valores.

Por ejemplo, si quisiéramos cambiar PART_TIME_EMPLOYEE a PT_EMPLOYEE, tendríamos problemas. Aunque este es un problema menos probable, cambiar los nombres de un tipo enumerado obligaría a cambiar todo el código que utiliza ese tipo enumerado, lo cual sería más engorroso que reasignar valores en una columna de base de datos.

Almacenar el ordinal es la mejor y más eficiente manera de manejar los tipos enumerados, siempre y cuando la probabilidad de agregar nuevos valores en el medio no sea alta. Se podrían agregar nuevos valores al final del tipo sin consecuencias negativas.

Consejo

Es posible tener valores enumerados que contengan estado. Actualmente, no hay soporte en Jakarta Persistence para mapear el estado contenido dentro de los valores enumerados, pero hay alguna estrategia que veremos más adelante o en ejercicios.

7.2. Mapeo de enumeraciones con @PostLoad y @PrePersist

Otra opción para la persistencia de enumeraciones es utilizar los métodos del estándar de JPA. Podemos mapear enumeraciones de ida (preescritrua) y vuelta (después de la carga) en los eventos @PostLoad y @PrePersist:

  • @PostLoad: se invoca después de que se cargue una entidad de la base de datos. PostLoad.
  • @PrePersist: se invoca antes de que se persista una entidad en la base de datos. PrePersist.

La idea es tener dos atributos en la entidad:

  • El primero se mapea a un valor de base de datos.
  • El segundo es un campo @Transient que almacena un valor real de la enumeración, que es utilizado por el código de lógica de negocio.

Por ejemplo:

public enum Prioridad {
    BAJA(100), MEDIA(200), ALTA(300);

    private int prioridad;

    private Prioridad(int prioridad) {
        this.prioridad = prioridad;
    }

    public int getPrioridad() {
        return prioridad;
    }

    public static Prioridad of(int prioridad) {
        return Stream.of(Prioridad.values())
          .filter(p -> p.getPrioridad() == prioridad)
          .findFirst()
          .orElseThrow(IllegalArgumentException::new);
    }
}

El método Prioridad.of() que hemos empleado muchas veces, facilita la obtención de una instancia de Prioridad basada en su valor entero.

En la entidad Articulo, añadimos los dos atributos e implantamos los métodos para lectura y escritura:

@Entity
public class Articulo {

    @Id
    private int id;

    private String titulo;

    @Enumerated(EnumType.ORDINAL) // Ejemplo con ORDINAL
    private Status estado;

    @Enumerated(EnumType.STRING) // Ejemplo con STRING
    private Tipo tipo;

    @Basic
    private int valorPrioridad; // Propiedad de base de datos

    @Transient
    private Prioridad prioridad; // Propiedad de negocio

    @PostLoad
    void completarTransient() {
        if (valorPrioridad > 0) {
            this.prioridad = Prioridad.of(valorPrioridad);
        }
    }

    @PrePersist
    void completarPersistente() {
        if (prioridad != null) {
            this.valorPrioridad = prioridad.getPrioridad();
        }
    }
}

Ahora, al persistir una entidad Articulo:

Articulo articulo = new Articulo();
articulo.setId(3);
articulo.setTitulo("Título ejemplo");
articulo.setPrioridad(Prioridad.ALTA);

JPA desencadenará la siguiente consulta SQL:

INSERT INTO Articulo (valorPrioridad, estado, titulo, tipo, id) 
VALUES (?, ?, ?, ?, ?)

binding parameter [1] as [INTEGER] - [300]
binding parameter [2] as [INTEGER] - [null]
binding parameter [3] as [VARCHAR] - [Título ejemplo]
binding parameter [4] as [VARCHAR] - [null]
binding parameter [5] as [INTEGER] - [3]

No es ideal tener dos atributos que representan una sola enumeración de la entidad. Además, si usamos este tipo de mapeo, no podemos utilizar el valor del enum en consultas JPQL.

7.3 Mapeo de enumeraciones con @Converter

La versión 2.1 de JPA introdujo una nueva API estandarizada que puede ser utilizada para convertir un atributo de entidad a un valor de base de datos y viceversa. Todo lo que necesitamos hacer es crear una nueva clase que implemente jakarta.persistence.AttributeConverter y anotarla con @Converter.

Una tercera opción es utilizar un @Converter. Un @Converter es una clase que implementa la interfaz AttributeConverter<X, Y>, donde X es el tipo de atributo de la entidad y Y es el tipo de columna de la base de datos. La interfaz AttributeConverter tiene dos métodos:

  • Y convertToDatabaseColumn(X attribute): convierte el atributo de la entidad en un tipo de columna de la base de datos.
  • X convertToEntityAttribute(Y dbData): convierte el tipo de columna de la base de datos en un atributo de la entidad.
  • Class<X> getJavaType(): devuelve el tipo de atributo de la entidad.
  • Class<Y> getDatabaseType(): devuelve el tipo de columna de la base de datos.
  • @Converter(autoApply = true): indica que el convertidor debe aplicarse a todos los atributos de la entidad que tengan el tipo de atributo X y el tipo de columna de la base de datos Y.

Primero, crearemos un nuevo enumerado:

public enum Categoria {
    DEPORTE("D"), MUSICA("M"), TECNOLOGIA("T");

    private String codigo;

    private Categoria(String codigo) {
        this.codigo = codigo;
    }

    public String getCodigo() {
        return codigo;
    }
}

También necesitamos agregarlo a la clase Articulo:

@Entity
public class Articulo {

    @Id
    private int id;

    private String titulo;

    @Enumerated(EnumType.ORDINAL)
    private Status estado;

    @Enumerated(EnumType.STRING)
    private Tipo tipo;

    @Basic
    private int valorPrioridad;

    @Transient
    private Prioridad prioridad;

    private Categoria categoria;
}

Ahora creemos un nuevo convertidor de categoría:

@Converter(autoApply = true)
public class ConvertidorCategoria implements AttributeConverter<Categoria, String> {
 
    @Override
    public String convertToDatabaseColumn(Categoria categoria) {
        if (categoria == null) {
            return null;
        }
        return categoria.getCodigo();
    }

    @Override
    public Categoria convertToEntityAttribute(String codigo) {
        if (codigo == null) {
            return null;
        }

        return Stream.of(Categoria.values())
          .filter(c -> c.getCodigo().equals(codigo))
          .findFirst()
          .orElseThrow(IllegalArgumentException::new);
    }
}

Hemos configurado el valor autoApply de @Converter en true para que JPA aplique automáticamente la lógica de conversión a todos los atributos mapeados de tipo Categoria. De lo contrario, tendríamos que poner la anotación @Convert directamente en el campo de la entidad.

Convert

https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/convert

La anotación @Convert se puede utilizar para aplicar un convertidor a un atributo específico de una entidad. Tiene como parámetros attributeName, converter y disableConversion.

  • attributeName: nombre del atributo de la entidad a la que será aplicado el convertidor
  • converter: clase del convertidor.
  • disableConversion: booleano que indica si se debe deshabilitar la conversión automática (por defecto false). Si está como true, no se aplicará el converter, y no debería estar indicado.
@Entity
public class Articulo {

// ...

    @Convert(converter = ConvertidorCategoria.class)
    private Categoria categoria;
}

Ahora persistamos una entidad Articulo:

Articulo articulo = new Articulo();
articulo.setId(4);
articulo.setTitulo("título convertido");
articulo.setCategoria(Categoria.MUSICA);

Entonces JPA ejecutará la siguiente instrucción SQL:

insert 
into
    Articulo
    (categoria, valorPrioridad, estado, titulo, tipo, id) 
values
    (?, ?, ?, ?, ?, ?)
Valor convertido al enlazar : MUSICA -> M
binding parameter [1] as [VARCHAR] - [M]
binding parameter [2] as [INTEGER] - [0]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [título convertido]
binding parameter [5] as [VARCHAR] - [null]
binding parameter [6] as [INTEGER] - [4]

Podemos establecer reglas para convertir enums a un valor de base de datos correspondiente si usamos la interfaz AttributeConverter. Además, podemos agregar nuevos valores de enum o cambiar los existentes sin romper los datos ya persistidos.

Es sencillo de implementar y supera las desventajas de las opciones presentadas en las secciones anteriores.

7.4. Uso de Enums en JPQL

Ahora veamos lo sencillo que es usar enums en las consultas JPQL.

Para encontrar todas las entidades Articulo con la categoría Categoria.DEPORTE:

String jpql = "select a from Articulo a where a.categoria = com.javhoz.ad.jpa.Categoria.DEPORTE";

List<Articulo> articulos = em.createQuery(jpql, Articulo.class).getResultList();

Es importante destacar que en este caso necesitamos utilizar el nombre completo de la enumeración.

Para consultas dinámicas, podemos utilizar parámetros con nombres:

String jpql = "select a from Articulo a where a.categoria = :categoria";

TypedQuery<Articulo> query = em.createQuery(jpql, Articulo.class);
query.setParameter("categoria", Categoria.TECNOLOGIA);

List<Article> articulos = query.getResultList();

Es la forma más adecuada, pues no se necesita utilizar nombres completamente calificados.

Ejercicio 05.03. Conversores personalizados y enumeraciones

Declara una entidad Persona con atributos:

  • idPersona.
  • nombre.
  • apellidos.
  • fechaNacimiento de tipo LocalDate.
  • sexo de tipo enumerado Sexo que puede ser HOMBRE o MUJER.
  • estadoCivil de tipo enumerado EstadoCivil que puede ser SOLTERO, CASADO, DIVORCIADO o VIUDO.
  • foto de tipo byte[].

Realiza las conversiones para que:

  • El nombre y apellidos se guardan en la base de datos como “apellidos, nombre”, con la primera letra de cada palabra en mayúsculas (empleando acceso por campo y por propiedad).
  • La fecha de nacimiento como un entero que representa la edad de la persona en años (obviamente no es la mejor forma de almacenar la edad, pero quiero que practiquéis con los convertidores), usando anotaciones @PostLoad y @PrePersist. Haz pruebas de comportamiento haciendo consultas, inserciones y actualizaciones.
  • Las enumeraciones se guardarán como cadenas en el caso de estado civil y como un carácter de ‘H’ o ‘M’ en el caso del sexo. Hazlo con conversores personalizados.
  • La fotografia se guardará en un campo de tipo BLOB.

Debes completar la entidad Persona y los convertidores necesarios para que funcione correctamente.

public class Persona {
    private long idPersona;
    private String nombre;
    private String apellidos;
    private LocalDate fechaNacimiento;
    private Sexo sexo;
    private EstadoCivil estadoCivil;
    private byte[] foto;
    // ...
}

Hazlo contra la base de datos H2 y comprueba que los datos se guardan correctamente, creando varios registros y recuperándolos.

8. Tipos temporales: @Temporal

Los tipos temporales se refieren al conjunto de tipos basados en el tiempo que se pueden utilizar en mapeos de estado persistentes.

La lista de tipos temporales admitidos incluye los tres tipos java.sql: java.sql.Date, java.sql.Time y java.sql.Timestamp, así como los dos tipos java.util: java.util.Date y java.util.Calendar, así como los tipos de java.time de Java 8.

Funcionan como cualquier otro tipo de mapeo simple, sin necesidad de consideraciones especiales.

Sin embargo, los dos tipos java.util.Date y java.util.Calendar necesitan metadatos adicionales para indicar cuál de los tipos java.sql de JDBC usar al comunicarse con el controlador JDBC y sólo pueden ser especificados en campos propiedades de estos dos tipo (o subclases). Esto se hace anotándolos con la anotación @Temporal y especificando el tipo JDBC como un valor del tipo enumerado TemporalType.

Tiene un único elemento value que es un valor de la enumeración TemporalType.Hay tres valores enumerados, que representan los tres tipos de la base de datos java.sql:

  • DATE.
  • TIME.
  • TIMESTAMP.

Por ejemplo, con java.util.Date y java.util.Calendar se pueden asignar a columnas de fecha en la base de datos:

@Entity
public class Employee {
    @Id
    private long id;
    
    @Temporal(TemporalType.DATE)
    private Calendar dob;
    
    @Temporal(TemporalType.DATE)
    @Column(name="S_DATE")
    private Date startDate;
    
    // ...
}
JPA 3.2 y superior

En JPA 3.2 y superior, esta anotación está desaprobada (deprecated). Se recomienda utilizar los tipos de la API de fecha y hora de Java 8 (java.time). Si se necesita persistir un tipo de fecha, se debe utilizar la anotación @Convert con un convertidor de atributos.

9. Atributos transitorios

Los atributos que no se pretende que sean persistentes pueden modificarse con el modificador transient en Java o con la anotación @Transient. El tiempo de ejecución del proveedor no aplicará sus reglas de mapeo predeterminadas al atributo en el que se especificó.

Los campos transitorios se utilizan por diversas razones:

Podría ser el caso anteriormente mencionado cuando mezclamos el modo de acceso y no queríamos persistir el mismo estado dos veces. Otra razón podría ser cuando se desea almacenar en caché algún estado en memoria que no se desea volver a calcular, redescubrir o reinicializar. Por ejemplo siguiente, se usa un campo transient para guardar el nombre específico del idioma para “Employee” de modo que lo imprimamos correctamente donde sea que se muestre. Hemos utilizado el modificador transient en lugar de la anotación @Transient para que si el Employee se serializa de una VM a otra, entonces el nombre traducido se reinicializará para corresponder al idioma de la nueva VM. En casos en los que el valor no persistente debe conservarse durante la serialización, se debe utilizar la anotación en lugar del modificador.

A continuación se muestra un ejemplo de cómo se utilizaría un campo transitorio:

@Entity
public class Employee {
    @Id
    private long id;
    
    private String name;
    private long salary;
    
    transient private String translatedName;
    
    // ...
    
    public String toString() {
        if (translatedName == null) {
            translatedName = ResourceBundle.getBundle("EmpResources").getString("Employee");
        }
        return translatedName + ": " + id + " " + name;
    }
}

10. Mapeo de clave primaria: @Id

  • Cualquier entidad debe tener un mapeo a una clave primaria en la tabla.

  • @Id indica el identificador de la entidad.

Nota: Cuando el identificador de una entidad está compuesto solo por un atributo, se llama un identificador simple.

10.1. Sobrescritura de la clave primaria

Se puede usar la anotación @Column para sobrescribir el nombre de la columna al que se asigna el atributo ID.

Las claves primarias son insertables, pero no nulas ni actualizables.

Con la anotación @Column, los elementos nullable y updatable no deben ser anulados. Sólo al asignar la misma columna a varios campos/relaciones, se debe establecer el elemento insertable en false.

10.2. Tipos de claves primarias

Los mapeos de @Id generalmente están restringidos a los siguientes tipos:

  • Tipos primitivos de Java: byte, int, short, long y char.
  • Clases envolventes de tipos primitivos de Java: Byte, Integer, Short, Long y Character.
  • Cadena: java.lang.String
  • Tipo numérico grande: java.math.BigInteger
  • Tipos temporales: java.util.Date y java.sql.Date, java.util.Calendar y java.sql.Timestamp, además de todos los subtipos y las Java 8 java.time API:
    • java.time.LocalDate
    • java.time.LocalTime
    • java.time.LocalDateTime
    • java.time.OffsetTime
    • java.time.OffsetDateTime
Float/Double para claves primarias

Se permiten tipos de punto flotante como float y double, así como las clases envolventes Float y Double y java.math.BigDecimal, pero se desaconsejan debido a la naturaleza del error de redondeo y la poca confiabilidad del operador equals() cuando se aplica a ellos. Utilizar tipos flotantes para claves primarias es arriesgado y definitivamente no se recomienda.

10.3. Generación de claves primarias: @GeneratedValue

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/generatedvalue

La generación de ID y se especifica mediante la anotación @GeneratedValue.

El proveedor de persistencia generará un valor de identificador para cada instancia de ese tipo de entidad.

Dependiendo de cómo se genere, es posible que en realidad no esté presente en el objeto hasta que la entidad se haya insertado en la base de datos, hasta después de que se haya producido un flush o la transacción haya finalizado.

Existen 5 tipos estrategias de generación de ID, especificando en el elemento strategy a alguno de los valores de la enumeración GenerationType:

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/generationtype

  • AUTO: el proveedor de persistencia debería seleccionar una estrategia apropiada para la base de datos particular.
  • IDENTITY: asigna claves primarias para la entidad utilizando una columna de identidad de base de datos.
  • SEQUENCE: asigna las claves primarias para la entidad utilizando una secuencia de base de datos.
  • TABLE: asigna claves primarias para la entidad utilizando una tabla de base de datos subyacente para garantizar la unicidad.
  • UUID: asigna las claves primarias para la entidad mediante la generación de un Identificador Único Universal según la norma RFC 4122. El tipo de atributo debe ser java.util.UUID;

Para obtener más detalles, puedes consultar la documentación oficial en la API de JPA 3.1.

Por ejemplo:

@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

Los generadores de tabla y secuencia pueden definirse específicamente y luego reutilizarse por múltiples clases de entidad. Estos generadores tienen un nombre y son globalmente accesibles para todas las entidades en la unidad de persistencia.

10.3.1. Generación Automática de ID: GenerationType.AUTO

La estrategia de AUTO el proveedor utilizará cualquier estrategia que desee para generar identificadores.

Se crea un valor de identificador por el proveedor e insertado en el campo id de cada entidad Employee que se persista.

Consejo: no se requiere explícitamente que el campo del identificador de la entidad sea de tipo entero, pero generalmente es el único tipo que genera AUTO. Se recomienda emplear long para abarcar toda la extensión del dominio del identificador generado.

Ejemplo. Uso de la Generación Automática de ID

@Entity
public class Employee {
 @Id @GeneratedValue(strategy=GenerationType.AUTO)
 private long id;
 // ...
}

Un inconveniente al usar AUTO es que el proveedor elige su propia estrategia para almacenar los identificadores, pero si elige una estrategia basada en tabla, necesita crear una tabla, por lo que necesita permisos para crear una tabla en la base de datos.

AUTO es realmente una estrategia de generación para desarrollo o prototipado. En cualquier otra situación, sería mejor usar una de las otras estrategias de generación.

10.3.2. Generación de ID utilizando una tabla: GenerationType.TABLE

La forma más flexible y portátil de generar identificadores es utilizar una tabla de base de datos. Se puede adaptar a diferentes bases de datos y permite almacenar múltiples secuencias de identificadores diferentes para diferentes entidades dentro de la misma tabla.

Una tabla de generación de ID debe tener dos columnas:

  • La primera columna es de tipo cadena y se utiliza para identificar la secuencia del generador en particular. Es la clave primaria para todos los generadores en la tabla.

  • La segunda columna es de tipo entero y almacena la secuencia de ID real que se está generando. El valor almacenado en esta columna es el último identificador que se asignó en la secuencia.

  • Cada generador definido representa una fila en la tabla.

@Id
@GeneratedValue(strategy=GenerationType.TABLE)
private long id;

Existen varios enfoques para definir un generador de tabla:

  • El enfoque más sencillo es no definir ningún generador y dejar que el proveedor cree la tabla. Si se utiliza la generación (create) de esquema, se creará; si no, la tabla predeterminada asumida por el proveedor debe ser conocida y debe existir en la base de datos.

  • Un enfoque más preciso es especificar la tabla que se utilizará para almacenar el ID. Esto se hace definiendo un generador de tabla que no crea tablas en realidad, pues es un generador de identificadores que utiliza una tabla para almacenar los valores del identificador

Podemos definir uno usando la anotación @TableGenerator y luego hacer referencia a él por nombre en la anotación @GeneratedValue:

@TableGenerator(name ="Emp_Gen")
@Id
@GeneratedValue(generator="Emp_Gen")
private long id;
@Entity
public class Empleado {
    ...
    @TableGenerator(
        name = "generadorEmpleado",
        table = "ID_GEN",
        pkColumnName = "GEN_KEY",
        valueColumnName = "GEN_VALUE",
        pkColumnValue = "EMP_ID",
        allocationSize = 1)
    @Id
    @GeneratedValue(strategy = TABLE, generator = "generadorEmpleado")
    int id;
    ...
}

Aunque en el ejemplo se indica la anotación @TableGenerator anotando el atributo del identificador se puede definir en cualquier atributo o clase. El elemento name nombra globalmente al generador, lo que nos permite hacer referencia a él en el elemento generator de la anotación @GeneratedValue. Pero no aprovechamos la flexibilidad de la tabla de generación de ID, pues no hemos definido ninguna de las propiedades opcionales.

Independientemente de dónde se defina, estará disponible para toda la unidad de persistencia.

Es una buena práctica definirla localmente en el atributo de ID si solo una clase la está utilizando, y definirla en XML, si se va a utilizar para varias clases.

Elementos de la anotación @TableGenerator:

  • name: nombre del generador (opcional). El valor por predeterminado es el nombre de la entidad cuando la anotación se produce en una entidad o en una clave primaria.

  • table: nombre de la tabla que almacena los valores de la secuencia de ID (opcional). El valor por defecto lo elige el proveedor de persistencia.

  • catalog: catálogo de la tabla (opcional). Catálogo por defecto.

  • schema: esquema de la tabla (opcional). Esquema por defecto para el usuario actual.

  • pkColumnName: nombre de la columna de clave primaria en la tabla que identifica de manera única al generador (opcional). Por defecto lo elige el proveedor de persistencia.

  • valueColumnName: nombre de la columna que almacena el valor real de la secuencia de ID que se está generando (opcional). Por defecto lo elige el proveedor de persistencia.

  • pkColumnValue: valor de clave primaria en la tabla generadora que distingue este conjunto de valores generados de otros que pueden almacenarse en la tabla. El valor predeterminado es un valor elegido por el proveedor para almacenar en la columna de clave principal de la tabla del generador (opcional).

  • initialValue: valor inicial de la secuencia de ID (opcional).

  • allocationSize: tamaño de asignación de la secuencia de ID (opcional).

  • uniqueConstraints: restricciones de unicidad de la tabla (opcional).

  • indexes: índices de la tabla (opcional).

Un enfoque más cualificado sería especificar los detalles de la tabla::

@TableGenerator(name="Emp_Gen", table="ID_GEN", pkColumnName="GEN_NAME",  valueColumnName="GEN_VAL")

Se ha incluido algunos elementos adicionales después del nombre del generador. Después del nombre, hay tres elementos: table, pkColumnName y valueColumnName, que definen la tabla real que almacena los identificadores para Emp_Gen. En el ejemplo:

La tabla se llama ID_GEN, el nombre de la columna de clave primaria (la columna que almacena los nombres de los generadores) se llama GEN_NAME, y la columna que almacena los valores de la secuencia de ID se llama GEN_VAL.

El nombre del generador se convierte en el valor almacenado en la columna pkColumnName para esa fila y es utilizado por el proveedor para buscar el generador y obtener su último valor asignado.

El elemento initialValue que representa el último identificador asignado puede especificarse como parte de la definición del generador, pero la configuración predeterminada de 0 será suficiente en casi todos los casos. Esta configuración solo se utiliza durante la generación de esquemas cuando se crea la tabla. Durante ejecuciones posteriores, el proveedor leerá el contenido de la columna de valores para determinar el próximo identificador a asignar.

Para evitar actualizar la fila cada vez que se solicita un identificador, se utiliza un tamaño de asignación. Esto hará que el proveedor preasigne un bloque de identificadores y luego asignará identificadores desde la memoria según sea necesario hasta que se agote el bloque. Una vez que se agota este bloque, la próxima solicitud de un identificador activará otro bloque de identificadores para preasignar y el valor del identificador se incrementará por el tamaño de asignación. De forma predeterminada, el tamaño de asignación está configurado en 50. Este valor puede anularse para ser más grande o más pequeño mediante el uso del elemento allocationSize al definir el generador.

Ejemplo de cómo definir un segundo generador que se utilizará para entidades de dirección pero que utiliza la misma tabla ID_GEN para almacenar la secuencia de identificadores.

Precisamos indicar el valor que estamos almacenando en la columna de clave en elemento pkColumnValue. Este elemento permite que el nombre del generador sea diferente del valor de la columna:

Especifica un generador de ID de dirección llamado Address_Gen, pero luego define el valor almacenado en la tabla para la generación de ID de dirección como Addr_Gen. El generador también establece el valor inicial en 10000 y el tamaño de asignación en 100.

@TableGenerator(name="Address_Gen",
 table="ID_GEN",
 pkColumnName="GEN_NAME",
 valueColumnName="GEN_VAL",
 pkColumnValue="Addr_Gen",
 initialValue=10000,
 allocationSize=100)
@Id @GeneratedValue(generator="Address_Gen")
private long id;

Si no se ha indicado “create” o “drop-and-create, la tabla debe existir o crearse en la base de datos a través de algún otro medio y configurarse para estar en este estado cuando la aplicación se inicie por primera vez:

CREATE TABLE id_gen (
 gen_name VARCHAR(80),
 gen_val INTEGER,
 CONSTRAINT pk_id_gen
 PRIMARY KEY (gen_name)
);
INSERT INTO id_gen (gen_name, gen_val) VALUES ('Emp_Gen', 0);
INSERT INTO id_gen (gen_name, gen_val) VALUES ('Addr_Gen', 10000);
Ejercicio 05.04. Generación de ids con tabla

A partir del ejecicio anterior con Persona, haz aque el campo idPersona de tipo Long y genera el identificador con una tabla. La tabla debe ser compartida con otras entidades que tengan un campo id de tipo Long.

  • Nombre de la tabla: LONG_ID_GEN
  • Columnas:
    • nomePK.
    • valorPK.
    • El valor de la columna nomePK para la entidad Persona debe ser PERSONA_ID.
    • Dale un valor inicial de 1000 y un tamaño de asignación de 100.

Crea otro generador para esa tabla que se utilizará para la entidad Direccion con un valor inicial de 2000 y un tamaño de asignación de 50.

Haz pruebas de inserción de datos.

10.3.3. Generación de ID Utilizando una Secuencia de Base de Datos: GenerationType.SEQUENCE

Muchas bases de datos admiten un mecanismo interno de generación de ID llamado secuencias.

Una secuencia de base de datos se puede utilizar para generar identificadores cuando la base de datos subyacente las admite.

Secuencia de una base de datos

Una secuencia de base de datos es un objeto de base de datos que genera una secuencia de números únicos. Cada vez que se solicita un número de secuencia, se genera el siguiente número de secuencia. Las secuencias de base de datos son muy eficientes y se pueden asignar en bloques. Esto significa que el proveedor puede asignar un bloque de identificadores de la base de datos a la memoria y luego asignar identificadores desde la memoria hasta que se agote el bloque. Una vez que se agota el bloque, el proveedor solicitará otro bloque de identificadores de la base de datos. Esto reduce la cantidad de comunicación necesaria con la base de datos y mejora el rendimiento.

@Id @GeneratedValue(strategy=GenerationType.SEQUENCE)
private long id;

La única diferencia entre usar una secuencia para varios tipos de entidad y usar una para cada entidad sería el orden de los números de secuencia y la posible competencia en la secuencia. La opción más segura es definir un generador de secuencias con nombre y hacer referencia a él en la anotación @GeneratedValue:

@SequenceGenerator(name="Emp_Gen", sequenceName="Emp_Seq")
@Id
@GeneratedValue(generator="Emp_Gen")
private long id;

Se requeriría que la secuencia esté definida y ya exista:

CREATE SEQUENCE Emp_Seq
 MINVALUE 1
 START WITH 1
 INCREMENT BY 50

Si no se utiliza la generación de esquema y la secuencia se crea manualmente, la cláusula INCREMENT BY debería configurarse para que coincida con el elemento allocationSize o el tamaño de asignación predeterminado de la anotación @SequenceGenerator correspondiente.

Ejercicio 05.05. Generación de ids con una secuencia

Repite el ejercicio anterior con Persona, pero esta vez utiliza una secuencia para generar el identificador en una base de datos H2. Haz pruebas compartiendo la secuencia y sin compartirla. Si puedes, haz lo mismo con una base de datos PostgreSQL.

10.3.4. Generación de ID utilizando una Identidad de Base de Datos

Muchas bases de datos admiten una columna de identidad de clave primaria, a veces denominada columna autonumérica.

La identidad se usa a menudo cuando las secuencias de bases de datos no son compatibles con la base de datos o porque un esquema heredado ya ha definido que la tabla utilice columnas de identidad.

Generalmente, son menos eficientes para la generación de identificadores objeto-relacional porque no se pueden asignar en bloques y porque el identificador no está disponible hasta después del tiempo de commit.

Para indicar que la generación de IDENTIDAD debe ocurrir, la anotación @GeneratedValue debe especificar una estrategia de generación de IDENTITY:

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private long id;

No hay una anotación de generador para IDENTITY porque debe definirse como parte de la definición del esquema de la base de datos para la columna de clave primaria de la entidad.

La generación de IDENTITY no se puede compartir entre varios tipos de entidades.

El identificador no será accesible hasta después de que se haya realizado la inserción. Es la acción de la inserción la que hace que se genere el identificador. Esto significa que no se puede utilizar el identificador en una relación bidireccional hasta después de que se haya realizado la inserción.

Al usar IDENTITY, algunos proveedores insertan entidades (cuando se invoca el método persist) que están configuradas para usar la generación de IDENTITY, en lugar de esperar hasta el tiempo de commit.__

10.3.5. Generación de ID Utilizando un UUID: GenerationType.UUID

Incorporado en JPA 3.1, el proveedor de persistencia generará un identificador único universal (UUID) para cada instancia de esa entidad.

@Id @GeneratedValue(strategy=GenerationType.UUID)
private UUID id;

El tipo de atributo debe ser java.util.UUID.

11. Ejercicio. Persistencia de una biblioteca

Ejercicio 05.06. Ampliación de la aplicación de persistencia de una biblioteca

Amplía el ejercicio de la biblioteca para que la entidad Book tenga un identificador generado automáticamente por medio de una tabla.

Además:

  • Crea una enumeración llamada Categoría con los siguientes valores: NOVELA, POESIA, ENSAYO, TEATRO y OTROS.

  • Haz que la entidad Book tenga un atributo de tipo Categoría y que se persista en la base de datos como una cadena. Realiza una conversión de la enumeración a cadena y viceversa de modo que guarde la categoría con el nombre en mayúsculas sólo la primera letra y con acentos.

  • Haz que la columna ISBN sea única, de un tamaño de 13 caracteres y que no pueda ser nula.

  • Crea un atributo de tipo Calendar para la fecha de publicación del libro y haz que se persista en la base de datos como un tipo DATE.

  • Crea un atributo transitorio que sea el número de días que han pasado desde la fecha de publicación hasta la fecha actual. Utiliza la clase java.time.LocalDate para obtener la fecha actual.

  • Crea otro atributo transitorio con el ISBN en versión de 10 dígitos, teniendo en cuenta que el ISBN es un número de 13 dígitos. Para ello, puedes utilizar la clase java.math.BigInteger para realizar la conversión y el siguiente algoritmo:

    1. Elimina los primeros tres dígitos (normalmente 978)
    2. Elimina el último dígito. Ahora tienes nueve dígitos
    3. Ahora necesitas calcular el ‘dígito de control’, que será el décimo dígito de tu ISBN. El objetivo del dígito de control es asegurarse de no haber cometido un error tipográfico: transponer dos dígitos, por ejemplo, o escribir mal uno. Esto es bastante complicado:
    4. Multiplica el primer dígito por 10, el segundo por 9, el tercero por 8 y así sucesivamente, hasta llegar al último dígito (multiplicado por 2).
    5. Ahora tienes una cadena de 9 números nuevos. Agrégalos todos juntos.
    6. Divide esta suma por once. Ahora estás interesado en el resto. Por ejemplo, si la suma fuera 242, que es exactamente 11 x 22, entonces el resto es cero. Si la suma fuera 243, entonces sobraría 1. Tendrás un resto que está entre 0 y 10.
    7. Resta ese resto de 11 para obtener el dígito de control.
    8. Si el resultado es 10, entonces el dígito de control es ‘X’.

Código Java:

    public class ISBN {
        public static void main(String[] args) {
            String isbn = "978-3-16-148410-0";
            String isbn10 = isbn.substring(3, isbn.length() - 1);
            System.out.println(isbn10);
            BigInteger sum = BigInteger.ZERO;
            for (int i = 0; i < isbn10.length(); i++) {
                int digit = Character.getNumericValue(isbn10.charAt(i));
                sum = sum.add(BigInteger.valueOf(digit).multiply(BigInteger.valueOf(10 - i)));
            }
            System.out.println(sum);
            BigInteger remainder = sum.mod(BigInteger.valueOf(11));
            System.out.println(remainder);
            BigInteger controlDigit = BigInteger.valueOf(11).subtract(remainder);
            System.out.println(controlDigit);
            if (controlDigit.intValue() == 10) {
                System.out.println("X");
            } else {
                System.out.println(controlDigit);
            }
        }
    }

Un ejemplo más completo:

   public class ISBNConverter {

    public static void main(String[] args) {
        String isbn13 = "9780123456789"; // ISBN-13
        String isbn10 = convertirISBN13aISBN10(isbn13);
        System.out.println("ISBN-10: " + isbn10);
    }

    public static String convertirISBN13aISBN10(String isbn13) {
        // Verifica si el ISBN-13 proporcionado es válido
        if (!esISBN13Valido(isbn13)) {
            return "ISBN-13 no válido";
        }

        // Elimina los primeros 3 dígitos (978 o 979) del ISBN-13
        String isbn10Parcial = isbn13.substring(3);

        // Calcula el dígito de verificación para el ISBN-10 parcial
        int suma = 0;
        for (int i = 0; i < 9; i++) {
            int digito = Character.getNumericValue(isbn10Parcial.charAt(i));
            suma += (i + 1) * digito;
        }

        int digitoVerificador = suma % 11;
        char digitoVerificadorChar;

        if (digitoVerificador == 10) {
            digitoVerificadorChar = 'X';
        } else {
            digitoVerificadorChar = (char) ('0' + digitoVerificador);
        }

        // Combina el ISBN-10 parcial con el dígito de verificación calculado
        return isbn10Parcial + digitoVerificadorChar;
    }

    public static boolean esISBN13Valido(String isbn13) {
        // Verifica que el ISBN-13 tenga 13 dígitos y comience con "978" o "979"
        return isbn13.matches("^97[89]\\d{10}$");
    }
}

Crea varios libros y pérsistelos en la base de datos (una nueva). Recupéralos y muestra los valores de los datos, incluyendo transitorios.

Última actualización: 23.09.2025

06. Relaciones JPA.

1. Relaciones entre entidades

La mayoría de las entidades necesitan referenciar o tener relaciones con otras entidades. Es lo que produce un modelo gráfico de entidades y relaciones común en las aplicaciones de negocio.

En JPA, las relaciones entre entidades se definen mediante anotaciones en los atributos/propiedades de las entidades.

En este apartado vamos a ver cómo se pueden definir relaciones entre entidades en JPA.

Las anotaciones que se utilizan son las siguientes:

  • @OneToOne: relación uno a uno.
  • @OneToMany: relación uno a muchos.
  • @ManyToOne: relación muchos a uno.
  • @ManyToMany: relación muchos a muchos.

Además, también se emplean otras anotaciones que permiten concretar y especificar los cuatro tipos de relaciones:

  • @Embedded: define una relación de tipo embebida (una entidad embebida en otra).
  • @ElementCollection: definir una relación de tipo colección (una relación uno a muchos en la que hay una dependencia entre las entidades).
  • @JoinColumn: define el nombre de la columna que se utilizará para la relación.
  • @JoinTable: permite definir el nombre de la tabla que se utilizará para la relación.
  • @MapKey: permite definir el nombre de la columna que se utilizará como clave en una relación de tipo mapa.
  • @OrderBy: nombre de la columna que se utilizará para ordenar los elementos de una relación.
  • @OrderColumn: nombre de la columna que se utilizará para ordenar los elementos de una relación.
  • @Index: Permite definir el nombre de la columna que se utilizará para crear un índice en una relación.
  • @ForeignKey: nombre de la columna que se utilizará para crear una clave foránea en una relación.
  • @AssociationOverride: nombre de la columna que se utilizará para crear una clave foránea en una relación.
  • @AttributeOverride: Permite definir el nombre de la columna que se utilizará para crear una clave foránea en una relación.
  • @EmbeddedId: definir una clave primaria compuesta.
  • @IdClass: definir una clave primaria compuesta.

1.1. Roles de las entidades en las relaciones

En cada relación hay dos entidades que están relacionadas, y cada entidad se dice que tiene un rol en la relación.

Los dos roles son:

  • Una entidad tiene un rol de propietario.
  • otra entidad tiene un rol de inversor.

El rol de propietario determina cómo se actualiza la relación en la base de datos.

El elemento mappedBy en la anotación de la relación designa la propiedad o campo en la entidad que es el propietario de la relación.

1.2. Direccionalidad de las relaciones

El modo más sencillo de implantar relaciones es que una entidad tenga un atributo que referencia a otra entidad, que identifica el papel que juega en la relación.

Además, es usual que la otra entidad tenga un atributo que apunte a la entidad original (relación bidireccional).

Las relaciones entre entidades pueden ser:

  • Unidireccionales: cuando sólo un atributo apunta a la otra entidad (es el lado propietario).
  • Bidireccionales: cuando cada entidad tiene un/os atributo/s que referencian a la otra entidad.

Más concretamente:

  • Una relación bidireccional tiene un lado propietario (owning) y un lado inverso (non-owning).

  • Una relación unidireccional tiene solo un lado propietario. El lado propietario de una relación determina las actualizaciones de la relación en la base de datos.

Relación unidireccional:

La otra entidad no tiene referencia a la primera entidad. Por ejemplo, en una relación unidireccional uno a muchos, la entidad que representa el lado “uno” de la relación tiene una referencia a la entidad que representa el lado “muchos” de la relación, pero la entidad que representa el lado “muchos” de la relación no tiene referencia a la entidad que representa el lado “uno” de la relación.

Por ejemplo, una relación unidireccional uno a uno entre las entidades Empleado y Dirección se puede definir de la siguiente manera:

Empleado-Direccion Empleado-Direccion

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    @OneToOne
    private Direccion direccion; // Empleado tiene una referencia a Direccion
    // ...
}

Se creará una tabla Empleado con una columna direccion_idDireccion que será la clave foránea que referencia a la tabla Direccion. Se dice que Empleado es el propietario de la relación.

@Entity
public class Direccion {
    @Id
    private int idDireccion;
    private String calle;
    private String ciudad;
    private String provincia;
    private String pais;
    private String codigoPostal;
    // ...
}

La tabla Direccion no tiene referencia a la tabla Empleado.

A veces, las relaciones unidireccionales en el modelo de objetos son un problema en el modelo de la base.

Relación bidireccional:

En una relación bidireccional, cada entidad tiene una referencia a la otra entidad. Por ejemplo, en una relación bidireccional uno a muchos, la entidad que representa el lado “uno” de la relación tiene una referencia a la entidad que representa el lado “muchos” de la relación, y la entidad que representa el lado “muchos” de la relación tiene una referencia a la entidad que representa el lado “uno”

Por ejemplo, Empleado y Proyecto podría ser una relación bidireccional si el empleado tiene referencia de los proyectos en los que trabaja y el Proyecto tiene referencia de los objetos de tipo Empleado que trabajan en el Proyecto. Ejemplo de relación bidireccional entre las entidades Empleado y Proyecto:

Empleado-Proyecto Empleado-Proyecto

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    @ManyToMany
    private List<Proyecto> proyectos;
    // ...
}

La tabla Empleado no tiene referencia a la tabla Proyecto. Sin embargo, se creará una tabla de unión Empleado_Proyecto que contendrá las claves primarias de ambas tablas.

@Entity
public class Proyecto {
    @Id
    private int idProyecto;
    private String nombre;
    @ManyToMany(mappedBy="proyectos") 
    private List<Empleado> empleados;
    // ...
}

1.3. Cardinalidad de las relaciones

La cardinalidad de una relación es el número de instancias de una entidad que pueden estar asociadas con una instancia de la otra entidad.

La cardinalidad de una relación se especifica mediante el uso de las anotaciones @OneToOne, @OneToMany, @ManyToOne o @ManyToMany.

Por ejemplo, muchos a uno entre Empleado y Departamento:

Empleado-Departamento Empleado-Departamento

Empleado-Departamento Empleado-Departamento

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    @ManyToOne
    private Departamento departamento;
    // ...
}
@Entity
public class Departamento {
    @Id
    private int idDepartamento;
    private String nombre;
    @OneToMany(mappedBy="departamento")
    private List<Empleado> empleados;
    // ...
}

Empleado con Proyecto, muchos a muchos:

Empleado-Proyecto Empleado-Proyecto

1.4. Ordinalidad de las relaciones (opcionalidad)

La ordinalidad indica la necesidad de que exista una entidad destino cuando se crea una entidad.

Sirve para mostrar si la entidad de destino necesita ser especificada cuando se crea la entidad de origen. Dado que la ordinalidad es realmente sólo un valor booleano, también se le conoce como la opcionalidad de la relación.

En términos de cardinalidad, la ordinalidad se indica mediante la cardinalidad siendo un rango en lugar de un valor simple, y el rango comenzaría con 0 o 1 dependiendo de la ordinalidad.

Es más sencillo simplemente indicar que la relación es opcional o obligatoria. Si es opcional, el destino puede no estar presente; si es obligatoria, una entidad de origen sin una referencia a su entidad de destino asociada se encuentra en un estado no válido.


2. Relaciones entre Entidades

Las relaciones entre entidades pueden ser:

Si existe una asociación entre dos entidades, se debe aplicar una de las siguientes anotaciones de modelado de relaciones a la propiedad persistente correspondiente o al campo de la entidad referenciadora: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany. Para asociaciones que no especifican el tipo de destino (por ejemplo, cuando no se utilizan tipos genéricos de Java para colecciones), es necesario especificar la entidad que es el destino de la relación.

3. Relaciones mono-valuadas: OneToOne y ManyToOne

Son aquellas relaciones en las que la cardinalidad del destino es 1:

Las relaciones mono-valuadas son las que se establecen entre dos entidades y que se pueden representar mediante una única columna en la tabla de la entidad que representa el lado “muchos” de la relación (la entidad tiene un atributo simple que referencia a la otra entidad).

La entidad origen referencia a una entidad destino.

3.1. @OneToOne unidireccionales

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/onetoone

Un ejemplo de una asociación uno a uno sería un Empleado que tiene un Aparcamiento. Suponiendo que cada empleado tenga asignada su propia plaza de aparcamiento, crearíamos una relación uno a uno desde Empleado hasta Aparcamiento:

Empleado-Aparcamiento Empleado-Aparcamiento

Entidad propietaria de la relación:

La entidad Empleado tendría un atributo aparcamiento que referencia a la entidad Aparcamiento y se dice que es la entidad propietaria de la relación (tendrá una clave foránea relacionada con Apartamiento).

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @OneToOne
    private Aparcamiento aparcamiento;
    // ...
}
@Entity
public class Aparcamiento {
    @Id
    private int idAparcamiento;
    private int numero;
    private String direccion;
    // ...
}

Las tablas resultantes serían:

Empleado-Aparcamiento Empleado-Aparcamiento

Puede verse que la tabla Empleado tiene una columna aparcamiento_idAparcamiento que es la clave foránea que referencia a la tabla Aparcamiento.

Anotación @JoinColumn:

También es posible indicar la columna de la relación por medio de la anotación @JoinColumn, que permite sustituir el nombre de la columna predeterminada, aparcamiento_idAparcamiento, por una nueva:

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @OneToOne
    @JoinColumn(name="idAparcamiento")
    private Aparcamiento aparcamiento;
    // ...
}

Tablas resultantes:

Empleado-Aparcamiento Empleado-Aparcamiento

3.2. @OneToOne bidireccionales

La entidad objetivo de la relación uno a uno a menudo tiene una relación de vuelta a la entidad fuente; por ejemplo, Aparcamiento tiene una referencia de vuelta al Empleado que lo utiliza. Es lo que se llama a una relación bidireccional uno a uno.

Sólo se necesita añadir un atributo Aparcamiento para que apunte a Empleado:

Empleado-Aparcamiento Empleado-Aparcamiento

La tabla de entidad que contiene la columna de unión determina la entidad que es propietaria de la relación.
En una relación bidireccional uno a uno, ambas asignaciones son asignaciones de uno a uno, y cualquiera de los lados puede ser el propietario, por lo que la columna de unión podría terminar en uno u otro lado. Es una decisión de modelado de datos, no una decisión de programación Java.

Ahora tenemos que agregar una referencia de vuelta de Aparcamiento a Empleado. Esto se logra añadiendo la anotación de relación @OneToOne en un atributo empleado. Como parte de la anotación, debemos agregar un elemento mappedBy para indicar que el lado propietario es Empleado, no Aparcamiento.

Dado Aparcamiento es el lado inverso de la relación, no se puede suministrar la información de la columna de unión.

@Entity
public class Aparcamiento {
    @Id
    private int idAparcamiento;
    private int numero;
    private String direccion;
    
    @OneToOne(mappedBy="aparcamiento")
    private Empleado empleado;
    // ...
}

Empleado-Aparcamiento Empleado-Aparcamiento

Las dos reglas para asociaciones bidireccionales uno a uno son las siguientes:

  • La anotación @JoinColumn va en el mapeo de la entidad que está mapeada a la tabla que contiene la columna de unión, o el lado propietario de la relación. Esto podría estar en cualquiera de los lados de la asociación.

  • El elemento mappedBy debe especificarse en la anotación @OneToOne en la entidad que no define una columna de unión, o el lado inverso de la relación.

Aviso

No es legal tener una asociación bidireccional que tuviera mappedBy en ambos lados, al igual que tampoco incorrecto no tenerlo en ninguno de los lados. La diferencia es que si estuviera ausente en ambos lados de la relación, el proveedor trataría cada lado como una relación unidireccional independiente. Esto estaría bien, excepto que asumiría que cada lado era el propietario y que cada uno tenía una columna de unión.

Si le hubiésemos puesto @JoinColumn a la entidad Aparcamiento, la tabla resultante sería:

@Entity
public class Aparcamiento {
    @Id
    private int idAparcamiento;
    private int numero;
    private String direccion;
    
    @OneToOne
    @JoinColumn(name="idEmpleado")
    private Empleado empleado;
    // ...
}

Empleado-Aparcamiento Empleado-Aparcamiento

A continuación, pondremos un ejercicio de ejemplo de relación uno a uno bidireccional.

Ejercicio 06.01. Relación uno a uno bidireccional Equipo-Entrenador

Vamos a crear una aplicación de equipos de la NBA. Cada equipo tiene un entrenador y cada entrenador tiene un equipo, por lo que la relación es uno a uno bidireccional.

Crea las siguientes entidades:

  • Equipo: con los atributos idEquipo, nombre, ciudad, conferencia, division, nombreCompleto y abreviatura.
    • Crea una enumeración Conferencia con los valores ESTE y OESTE.
    • Crea una enumeración Division con los valores ATLANTICO, CENTRAL, SURESTE, NOROESTE, PACIFICO y SUROESTE.
    • En la base de datos, la conferencia y la división se guardarán como cadenas:
      • EAST, WEST
      • ATLANTIC, CENTRAL, SOUTHEAST, NORTHWEST, PACIFIC, SOUTHWEST
    • La abreviatura debe ser única, así como el idEquipo.

Los equipos puedes cargarlos del siguiente archivo JSON:

Ver datos de ejemplo
{
"Equipo": [
	{
		"abreviatura" : "ATL",
		"idEquipo" : 1,
		"ciudad" : "Atlanta",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Hawks",
		"nombreCompleto" : "Atlanta Hawks"
	},
	{
		"abreviatura" : "BOS",
		"idEquipo" : 2,
		"ciudad" : "Boston",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Celtics",
		"nombreCompleto" : "Boston Celtics"
	},
	{
		"abreviatura" : "BKN",
		"idEquipo" : 3,
		"ciudad" : "Brooklyn",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Nets",
		"nombreCompleto" : "Brooklyn Nets"
	},
	{
		"abreviatura" : "CHA",
		"idEquipo" : 4,
		"ciudad" : "Charlotte",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Hornets",
		"nombreCompleto" : "Charlotte Hornets"
	},
	{
		"abreviatura" : "CHI",
		"idEquipo" : 5,
		"ciudad" : "Chicago",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Bulls",
		"nombreCompleto" : "Chicago Bulls"
	},
	{
		"abreviatura" : "CLE",
		"idEquipo" : 6,
		"ciudad" : "Cleveland",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Cavaliers",
		"nombreCompleto" : "Cleveland Cavaliers"
	},
	{
		"abreviatura" : "DAL",
		"idEquipo" : 7,
		"ciudad" : "Dallas",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Mavericks",
		"nombreCompleto" : "Dallas Mavericks"
	},
	{
		"abreviatura" : "DEN",
		"idEquipo" : 8,
		"ciudad" : "Denver",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Nuggets",
		"nombreCompleto" : "Denver Nuggets"
	},
	{
		"abreviatura" : "DET",
		"idEquipo" : 9,
		"ciudad" : "Detroit",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Pistons",
		"nombreCompleto" : "Detroit Pistons"
	},
	{
		"abreviatura" : "GSW",
		"idEquipo" : 10,
		"ciudad" : "Golden State",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Warriors",
		"nombreCompleto" : "Golden State Warriors"
	},
	{
		"abreviatura" : "HOU",
		"idEquipo" : 11,
		"ciudad" : "Houston",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Rockets",
		"nombreCompleto" : "Houston Rockets"
	},
	{
		"abreviatura" : "IND",
		"idEquipo" : 12,
		"ciudad" : "Indiana",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Pacers",
		"nombreCompleto" : "Indiana Pacers"
	},
	{
		"abreviatura" : "LAC",
		"idEquipo" : 13,
		"ciudad" : "LA",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Clippers",
		"nombreCompleto" : "LA Clippers"
	},
	{
		"abreviatura" : "LAL",
		"idEquipo" : 14,
		"ciudad" : "Los Angeles",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Lakers",
		"nombreCompleto" : "Los Angeles Lakers"
	},
	{
		"abreviatura" : "MEM",
		"idEquipo" : 15,
		"ciudad" : "Memphis",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Grizzlies",
		"nombreCompleto" : "Memphis Grizzlies"
	},
	{
		"abreviatura" : "MIA",
		"idEquipo" : 16,
		"ciudad" : "Miami",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Heat",
		"nombreCompleto" : "Miami Heat"
	},
	{
		"abreviatura" : "MIL",
		"idEquipo" : 17,
		"ciudad" : "Milwaukee",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Bucks",
		"nombreCompleto" : "Milwaukee Bucks"
	},
	{
		"abreviatura" : "MIN",
		"idEquipo" : 18,
		"ciudad" : "Minnesota",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Timberwolves",
		"nombreCompleto" : "Minnesota Timberwolves"
	},
	{
		"abreviatura" : "NOP",
		"idEquipo" : 19,
		"ciudad" : "New Orleans",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Pelicans",
		"nombreCompleto" : "New Orleans Pelicans"
	},
	{
		"abreviatura" : "NYK",
		"idEquipo" : 20,
		"ciudad" : "New York",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Knicks",
		"nombreCompleto" : "New York Knicks"
	},
	{
		"abreviatura" : "OKC",
		"idEquipo" : 21,
		"ciudad" : "Oklahoma City",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Thunder",
		"nombreCompleto" : "Oklahoma City Thunder"
	},
	{
		"abreviatura" : "ORL",
		"idEquipo" : 22,
		"ciudad" : "Orlando",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Magic",
		"nombreCompleto" : "Orlando Magic"
	},
	{
		"abreviatura" : "PHI",
		"idEquipo" : 23,
		"ciudad" : "Philadelphia",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "76ers",
		"nombreCompleto" : "Philadelphia 76ers"
	},
	{
		"abreviatura" : "PHX",
		"idEquipo" : 24,
		"ciudad" : "Phoenix",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Suns",
		"nombreCompleto" : "Phoenix Suns"
	},
	{
		"abreviatura" : "POR",
		"idEquipo" : 25,
		"ciudad" : "Portland",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Trail Blazers",
		"nombreCompleto" : "Portland Trail Blazers"
	},
	{
		"abreviatura" : "SAC",
		"idEquipo" : 26,
		"ciudad" : "Sacramento",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Kings",
		"nombreCompleto" : "Sacramento Kings"
	},
	{
		"abreviatura" : "SAS",
		"idEquipo" : 27,
		"ciudad" : "San Antonio",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Spurs",
		"nombreCompleto" : "San Antonio Spurs"
	},
	{
		"abreviatura" : "TOR",
		"idEquipo" : 28,
		"ciudad" : "Toronto",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Raptors",
		"nombreCompleto" : "Toronto Raptors"
	},
	{
		"abreviatura" : "UTA",
		"idEquipo" : 29,
		"ciudad" : "Utah",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Jazz",
		"nombreCompleto" : "Utah Jazz"
	},
	{
		"abreviatura" : "WAS",
		"idEquipo" : 30,
		"ciudad" : "Washington",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Wizards",
		"nombreCompleto" : "Washington Wizards"
	}
]}
  • Entrenador: con los atributos idEntrenador, nombre, fechaNacimiento, salario y equipo.

Mediante JPA e Hibernate, crea una aplicación que permita:

  • Añadir un equipo.
  • Insertar un entrenador.
  • Asignar un entrenador a un equipo.
  • Asignar un equipo a un entrenador.
  • Mostrar los datos de un equipo y su entrenador.

Para ello, debes crear las clases de utilidad necesarias para realizar las operaciones anteriores. JpaNbaManager, EquipoDAO, EntrenadorDAO, etc.

3.3. @ManyToOne unidireccional

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/manytoone

Se trata de una de las relaciones más comunes entre entidades.

Por tratarse de una relación unidireccional, sólo una entidad tiene una referencia a la otra entidad, la parte de “muchos” de la relación.

Por ejemplo, una relación muchos-a-uno entre Empleado y Departamento:

Relación Many-to-one de Empleado a Departamento Relación Many-to-one de Empleado a Departamento

Para ello se usa la anotación @ManyToOne.

Así la entidad Empleado queda del siguiente modo:

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @ManyToOne
    private Departamento departamento;
    // ...
}

La entidad Departamento no tiene referencia a la parte muchos, por lo que no se necesita ninguna anotación adicional:

@Entity
public class Departamento {
    @Id
    private int idDepartamento;
    private String nombre;
    // ...
}

Empleando @JoinColum

En una base de datos, las relaciones significan que una tabla referencia a otra. Cuando una columna referencia un clave (primaria) de otra tabla es lo que se denomina “Clave foránea”.

En JPA las claves foráneas se denominan “Join Columns” y, para ello, se emplea la anotación @JoinColum.

@JoinColumn y @JoinTable

La anotación @JoinColumn se utiliza para especificar una columna de clave foránea en una relación, usualmente, el nombre de la relación (name). Si la anotación @JoinColumn no se indica, el nombre de la columna de clave foránea se forma como el nombre de la propiedad o campo de relación de referencia de la entidad o clase embebible “_”; el nombre de la columna de clave primaria referenciada. Por ejemplo, si la relación es departamento en la entidad Empleado, la columna de clave foránea se llamará departamento_idDepartamento.

A veces, las @JoinColumn están dentro de otras tablas llamadas tablas de unión. En estos casos, se utiliza la anotación @JoinTable para especificar el nombre de la tabla de unión. Lo veremos en las relaciones multi-valuadas, como muchos-a-muchos.

Por ejemplo, si quisiéramos que la columna de la relación se llamara idDepartamento en lugar de departamento_idDepartamento, podríamos hacerlo de la siguiente manera:

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @ManyToOne
    @JoinColumn(name = "idDepartamento")
    private Departamento departamento;
    // ...
}

La columna idDepartamento se añadiría a la tabla Empleado y se referenciaría a la columna idDepartamento de la tabla Departamento.

Relación tablas Many-to-one de Empleado a Departamento con JoinColumn Relación tablas Many-to-one de Empleado a Departamento con JoinColumn

En la mayoría de las relaciones, independientemente de los lados fuente u origen, uno de los dos lados tiene una columna de clave foránea que referencia la clave primaria de la otra tabla. El lado que tiene la columna de clave foránea es el lado propietario de la relación.

La anotación @JoinColumn dispone de varios elementos:

  • name: nombre de la columna de clave foránea.
  • referencedColumnName: nombre de la columna referenciada por la columna de la clave foránea. Por ejemplo: @JoinColumn(name="idDepartamento", referencedColumnName="idDepartamento"). En dónde idDepartamento es el nombre de la columna de clave foránea y idDepartamento es el nombre de la columna referenciada.
  • nullable: indica si la columna de clave foránea puede ser nula.
  • unique: indica si la columna de clave foránea debe ser única.
  • insertable: indica si la columna de clave foránea debe incluirse en las operaciones de inserción.
  • updatable: indica si la columna de clave foránea debe incluirse en las operaciones de actualización.
  • columnDefinition: fragmento SQL que se usa para la generación del DDL de la columna de clave foránea.
IMPORTANTE: elemento mappedBy

La ausencia del elemento mappedBy en la anotación @ManyToOne indica que la relación es unidireccional. Si se especifica el elemento mappedBy en la entidad no propietaria (inversa, la que no tiene clave foránea), la relación es bidireccional. Además, su ausencia indica que es el propietario de la relación, mientras que la presencia de mappedBy indica que no es el propietario de la relación.

Ejercicio 06.02. Relación muchos a uno unidireccional Jugador-Equipo

Siguiendo el ejemplo anterior, vamos a crear una relación muchos a uno unidireccional entre Jugador y Equipo.

Para ello debe crear una nueva entidad Jugador con los siguientes atributos:

  • idJugador: identificador del jugador.
  • nombre: nombre del jugador.
  • apellidos: apellidos del jugador.
  • equipo: equipo al que pertenece el jugador.
  • altura: altura del jugador (Double).
  • peso: peso del jugador (Double).
  • numero: número de camiseta del jugador (SmallInt).
  • anoDraft: año de elección en el draft (entero).-
  • numeroDraft: número de elección en el draft (SmallInt).
  • rondaDraft: ronda de elección en el draft (SmallInt).
  • posicion: posición en la que juega (base, escolta, alero, ala-pívot, pívot, como enumeración, que debe guardarse como ‘G’, ‘C’, ‘F’, ‘F-C’, ‘C-F’).
  • pais: país de origen del jugador.
  • colegio: universidad o equipo en el que jugó.
  • foto: foto del jugador.

Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una referencia al Equipo y el nombre de la clave foránea sea idEquipo.

Crea jugadores y añádelos a los equipos que has creado en el ejercicio anterior. Completa la aplicación para que puedas añadir jugadores a los equipos y mostrar los jugadores de un equipo.

Datos de ejemplo:

Ver datos de ejemplo
{
"Jugador": [
	{
		"altura" : 198.12,
		"anoDraft" : 2013,
		"idEquipo" : 21,
		"idJugador" : 1,
		"numero" : 8,
		"numeroDraft" : 32,
		"peso" : 86.1825503,
		"posicion" : "G",
		"rondaDraft" : 2,
		"pais" : "Spain",
		"colegio" : "FC Barcelona",
		"nombre" : "Alex",
		"apellido" : "Abrines",
		"foto" : null
	},
	{
		"altura" : 182.88,
		"anoDraft" : null,
		"idEquipo" : 1,
		"idJugador" : 2,
		"numero" : 10,
		"numeroDraft" : null,
		"peso" : 102.05828325,
		"posicion" : "G",
		"rondaDraft" : null,
		"pais" : "USA",
		"colegio" : "St. Bonaventure",
		"nombre" : "Jaylen",
		"apellido" : "Adams",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2013,
		"idEquipo" : 11,
		"idJugador" : 3,
		"numero" : 12,
		"numeroDraft" : 12,
		"peso" : 120.20197805000001,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "New Zealand",
		"colegio" : "Pittsburgh",
		"nombre" : "Steven",
		"apellido" : "Adams",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2017,
		"idEquipo" : 16,
		"idJugador" : 4,
		"numero" : 13,
		"numeroDraft" : 14,
		"peso" : 115.66605435000001,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Kentucky",
		"nombre" : "Bam",
		"apellido" : "Adebayo",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2006,
		"idEquipo" : 3,
		"idJugador" : 6,
		"numero" : 21,
		"numeroDraft" : 2,
		"peso" : 113.3980925,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Texas",
		"nombre" : "LaMarcus",
		"apellido" : "Aldridge",
		"foto" : null
	},
	{
		"altura" : 193.04,
		"anoDraft" : 2018,
		"idEquipo" : 24,
		"idJugador" : 8,
		"numero" : 8,
		"numeroDraft" : 21,
		"peso" : 89.81128926000001,
		"posicion" : "G",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Duke",
		"nombre" : "Grayson",
		"apellido" : "Allen",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2017,
		"idEquipo" : 6,
		"idJugador" : 9,
		"numero" : 31,
		"numeroDraft" : 22,
		"peso" : 110.22294591,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Texas",
		"nombre" : "Jarrett",
		"apellido" : "Allen",
		"foto" : null
	},
	{
		"altura" : 203.2,
		"anoDraft" : 2010,
		"idEquipo" : 25,
		"idJugador" : 10,
		"numero" : 5,
		"numeroDraft" : 8,
		"peso" : 99.79032140000001,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Wake Forest",
		"nombre" : "Al-Farouq",
		"apellido" : "Aminu",
		"foto" : null
	},
	{
		"altura" : 195.57999999999998,
		"anoDraft" : 2015,
		"idEquipo" : 12,
		"idJugador" : 11,
		"numero" : 10,
		"numeroDraft" : 21,
		"peso" : 104.77983747,
		"posicion" : "G",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Virginia",
		"nombre" : "Justin",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2014,
		"idEquipo" : 18,
		"idJugador" : 12,
		"numero" : 1,
		"numeroDraft" : 30,
		"peso" : 104.32624510000001,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "UCLA",
		"nombre" : "Kyle",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 208.28,
		"anoDraft" : 2008,
		"idEquipo" : 19,
		"idJugador" : 13,
		"numero" : 31,
		"numeroDraft" : 21,
		"peso" : 108.8621688,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "California",
		"nombre" : "Ryan",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2013,
		"idEquipo" : 17,
		"idJugador" : 15,
		"numero" : 34,
		"numeroDraft" : 15,
		"peso" : 110.22294591,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "Greece",
		"colegio" : "Filathlitikos",
		"nombre" : "Giannis",
		"apellido" : "Antetokounmpo",
		"foto" : null
	},
	{
		"altura" : 208.28,
		"anoDraft" : 2018,
		"idEquipo" : 5,
		"idJugador" : 16,
		"numero" : 37,
		"numeroDraft" : 60,
		"peso" : 90.718474,
		"posicion" : "F",
		"rondaDraft" : 2,
		"pais" : "Greece",
		"colegio" : "Dayton",
		"nombre" : "Kostas",
		"apellido" : "Antetokounmpo",
		"foto" : null
	},
	{
		"altura" : 200.66,
		"anoDraft" : 2003,
		"idEquipo" : 14,
		"idJugador" : 17,
		"numero" : 7,
		"numeroDraft" : 3,
		"peso" : 107.95498406,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Syracuse",
		"nombre" : "Carmelo",
		"apellido" : "Anthony",
		"foto" : null
	},
	{
		"altura" : 200.66,
		"anoDraft" : 2017,
		"idEquipo" : 20,
		"idJugador" : 18,
		"numero" : 8,
		"numeroDraft" : 23,
		"peso" : 108.8621688,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "United Kingdom",
		"colegio" : "Indiana",
		"nombre" : "OG",
		"apellido" : "Anunoby",
		"foto" : null
	},
	{
		"altura" : 193.04,
		"anoDraft" : null,
		"idEquipo" : 24,
		"idJugador" : 30053472,
		"numero" : 19,
		"numeroDraft" : null,
		"peso" : 92.07925111,
		"posicion" : null,
		"rondaDraft" : null,
		"pais" : "Denmark",
		"colegio" : "CSKA Moscow",
		"nombre" : "Gabriel",
		"apellido" : "Lundberg",
		"foto" : null
	}
]}

4. Relaciones multi-valuadas

Las relaciones multi-valuadas son aquellas en las que la cardinalidad del destino es mayor que uno (muchos). Esto es, cuando una entidad puede estar asociada con más de una instancia de la otra entidad:

En este caso, se utilizan colecciones para representar las relaciones y es importante anotar la parte de la colección con: @OneToMany o @ManyToMany.

IMPORTANTE: mappedBy en relaciones OneToMany (y ManyToMany)

En las relaciones @OneToMany, la entidad que representa el lado “uno” de la relación suele estar indicada con el elemento mappedBy en la anotación @OneToMany para indicar que el lado inverso de la relación es el propietario de la relación y el que tiene la columna de clave foránea.

Aunque en una relación @ManyToMany no es preciso indicar quién es la entidad propietaria con mappedBy, es recomendable hacerlo para evitar problemas, y, además, poder especificar el nombre de la tabla de unión (en la que se almacenan las claves foráneas de ambas entidades).

4.1. @OneToMany

4.1.1. @OneToMany bidireccional

Cuando una entidad está asociada a una colección, Collection, (java.util.Collection) de otras entidades, se utiliza la anotación @OneToMany.

Una relación bidireccional one-to-many se establece mediante la anotación @OneToMany e implica una relación @ManyToOne en el lado opuesto de la relación, pues siempre implica una relación many-to-one en el lado opuesto de la relación.

En este tipo de relaciones, son (casi) siempre bidireccionales y el lado “UNO”, normalmente, NO es el propietario de la relación.

Por ejemplo, entre Departamento y Empleado:

Relación One-to-Many de Departamento a Empleado Relación One-to-Many de Departamento a Empleado

En el siguiente ejemplo, la entidad Departamento tiene una colección de Empleado y delega la responsabilidad de la relación a la entidad Empleado por el elemento mappedBy (la tabla Departamento no tendrá referencia a la tabla Empleado y la tabla Empleado tendrá una referencia a la tabla Departamento):

@Entity
public class Departamento {
    @Id
    private int idDepartamento;
    private String nombre;
    @OneToMany(mappedBy="departamento") // Empleado debe tener un atributo "departamento"
    private Collection<Empleado> empleados;
    // ...
}
@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @ManyToOne
    @JoinColumn(name="idDepartamento") // Nombre de la columna de clave foránea
    private Departamento departamento;
    // ...
}

El resultado es una tabla Empleado con la clave foránea del Departamento que referencia a la tabla Departamento:

Relación One-to-Many de Departamento a Empleado Relación One-to-Many de Departamento a Empleado

La única diferencia con la relación @ManyToOne es que se añade el atributo mappedBy en la anotación @OneToMany, que indica que el lado inverso de la relación es el propietario de la relación.

Es importante saber lo siguiente en las relaciones one-to-many o many-to-one bidireccionales:

  • El lado Many-To_One, que tiene la columna de clave foránea (@JoinColumn) es el lado propietario de la relación.

  • El lado One-To-Many, que tiene la anotación @OneToMany debe tener el elemento mappedBy, pues es el lado inverso de la relación. Si no se especifica mappedBy, el proveedor puede tratarlo como una relación unidireccional uno a muchos, que se define con una tabla intermedia.

Omisión de mappedBy en relaciones OneToMany

IMPORTANTE: si no se indica el elemento mappedBy en la anotación @OneToMany el proveedor puede tratarlo como una relación unidireccional uno a muchos, que se define con una tabla intermedia.

Es un error común no especificar mappedBy en el lado inverso de una relación bidireccionaluno a muchos . Si no se especifica mappedBy, el proveedor puede crear una tabla intermedia para la relación, que no es lo que se desea:

Relación One-to-Many de Departamento a Empleado con JoinColumn Relación One-to-Many de Departamento a Empleado con JoinColumn

Sólo en el caso de relaciones one-to-many unidireccionales se puede omitir mappedBy. Pues en ese caso la parte muchos no tiene referencia a la parte uno.

4.1.2 @OneToMany unidireccional

En algunos casos, la relación uno a muchos no tiene elemento mappedBy en la anotación @OneToMany, lo que indica que la relación es unidireccional. En ese caso la entidad muchos no tiene referencia a la entidad uno y sólo existe una colección en la entidad uno que referencia a la entidad muchos.

Por ejemplo, entre Empleado y Telefono podría verse como una relación unidireccional:

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @OneToMany
    @JoinTable(
        name="EmpleadoTelefono",
        joinColumns=@JoinColumn(name="idEmpleado"),
        inverseJoinColumns=@JoinColumn(name="idTelefono"))
    private Collection<Telefono> telefonos;
    // ...
}

Y la parte de la colección de la entidad Telefono no tendría referencia a la entidad Empleado:

@Entity
public class Telefono {
    @Id
    private int idTelefono;
    private String numero;
    private String tipo;
    // ...
}

Las tablas resultantes serían:

Relación One-to-Many de Empleado a Telefono con JoinTable Relación One-to-Many de Empleado a Telefono con JoinTable

Genéricos en colecciones:

JPA permite el uso de genéricos, por lo que se puede (y se debe) especificar el tipo de colección que se utilizará para la relación. Por ejemplo, Collection, List, Set, Map, etc. sin parametrizar.

En el caso de que se quiera emplear genéricos sin parametrizar, se debe especificar el tipo de la relación con la anotación @OneToMany y el elemento targetEntity:

@Entity
public class Departamento {
    @Id
    private int idDepartamento;
    private String nombre;
    @OneToMany(targetEntity=Empleado.class, mappedBy="departamento")
    private Collection empleados;
    // ...
}

Es opcional si la colección usa genéricos, en caso contrario debe especificarse el tipo de elemento con targetEntity.

4.2. @ManyToMany

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/manytomany

Cuando ambos lados de una relación de entidades tienen una asociación de una colección, se trata de una relación Muchos-a-muchos y se utiliza la anotación @ManyToMany.

Por ejemplo, entre Empleado y Proyecto:

Relación Many-to-Many de Empleado a Proyecto Relación Many-to-Many de Empleado a Proyecto

Ambos lados se mapean con la anotación @ManyToMany, especificando los parámetros de la tabla de unión con la anotación @JoinTable (evitamos los valores por defecto):

@Entity
public class Empleado {
    @Id
    private int idEmpleado;
    private String nombre;
    private long salario;
    @ManyToMany
    @JoinTable(name = "EmpleadoProyecto",
        joinColumns = @JoinColumn(name="idEmpleado"), // Si hubiera más de una columna, se especificaría con un array: 
            // joinColumns = {@JoinColumn(name="idEmpleado"), @JoinColumn(name="idOtraColumna")}
        inverseJoinColumns = @JoinColumn(name="idProyecto"))
    private Collection<Proyecto> proyectos;
    // ...
}

joinColumns es un array (JoinColumn[] joinColumns) y podría tener más de un elemento, si la tabla de unión tiene más de una columna de clave foránea:

{
        @JoinColumn(name="ADDR_ID", referencedColumnName="ID"),
        @JoinColumn(name="ADDR_ZIP", referencedColumnName="ZIP")
}
@Entity
public class Proyecto {
    @Id
    private int idProyecto;
    private String nombre;
    @ManyToMany
    @JoinTable(name = "EmpleadoProyecto",
        joinColumns = { @JoinColumn(name="idProyecto") },
        inverseJoinColumns = @JoinColumn(name="idEmpleado"))
    // ...
}

Queda pendiente que hagáis pruebas sin especificar @JoinTable para ver cómo se comporta con los valores por defecto. Del mismo modo, que sucede si no se especifica mappedBy en el lado inverso de la relación.

@Entity
public class Proyecto {
    @Id
    private int idProyecto;
    private String nombre;
    @ManyToMany(mappedBy="proyectos")
    private Collection<Empleado> empleados;
    // ...
}

Las tablas resultantes serían:

Relación Many-to-Many de Proyecto a Empleado con JoinTable Relación Many-to-Many de Proyecto a Empleado con JoinTable

Si no se indica la tabla de unión, el proveedor de JPA creará una tabla de unión con los valores por defecto:

Relación Many-to-Many de Proyecto a Empleado sin JoinTable Relación Many-to-Many de Proyecto a Empleado sin JoinTable

@JoinTable permite declarar toda la información sobre las columnas de la tabla de unión, como el nombre de la tabla, el nombre de las columnas de clave foránea, etc.
Los nombres de las columnas son en plural porque podría haber varias columnas por cada clave foránea (en el caso de una clave primaria con varias columnas).

IMPORTANTE: mappedBy en relaciones ManyToMany

Hay una importante diferencia entre las relaciones many-to-many y one-to-many:

  • Cuando muchos-a-muchos es bidireccional, ambos lados de la relación son muchos-a-muchos.

  • NO HAY columnas @JoinColumn en ninguna de las entidades, pues no hay un lado propietario de la relación y la única forma de mapear una relación muchos-a-muchos es con una tabla de unión (@JoinTable).

  • En una relación many-to-many, no hay un lado propietario de la relación. Ambos lados de la relación son iguales. Por ello, hay que especificar el lado propietario de la relación con el elemento mappedBy en la anotación @ManyToMany en el lado inverso de la relación.

  • Da igual cuál es el lado propietario, pero sólo se puede especificar mappedBy en uno de los lados de la relación.

5. Nombre de la columna de Clave foránea

El nombre de la columna de clave foránea (o de las tablas) se especifica con el elemento name de la anotación @JoinColumn. Si no se especifica, el nombre de la columna de clave foránea se genera automáticamente.

@Entity
public class Empleado {

    @Id private int idEmpleado;
    private String nombre;
    private long salario;

    @ManyToOne
    @JoinColumn(name = "idDepartamento")
    private Departamento departamento;
    // ...
}

El nombre de la tabla en la que se encuentra depende del contexto.

Dónde se encuentra la columna de clave foránea depende del tipo de relación y de la estrategia de mapeo de clave foránea:

  • Si la unión es para un mapeo OneToOne o ManyToOne utilizando una estrategia de mapeo de clave externa, la columna de clave externa está en la tabla de la entidad fuente o embebible.

  • Si la unión es para un mapeo unidireccional OneToMany utilizando una estrategia de mapeo de clave externa, la clave externa está en la tabla de la entidad objetivo.

  • Si la unión es para un mapeo ManyToMany o para un mapeo OneToOne o ManyToOne/OneToMany bidireccional utilizando una tabla de unión, la clave externa está en una tabla de unión.

  • Si la unión es para una colección de elementos, la clave foránea está en una tabla de colección. La colección de elementos lo veremos en otro apartado.

Ejercicio 06.03. Relación ManyToMany unidireccional Jugador-Posición

Vamos a crear una relación muchos a muchos unidireccional entre Jugador y Posicion. Para eso debes crear una nueva entidad Posicion con los siguientes atributos:

  • idPosicion: identificador de la posición (Long).
  • nombre: nombre de la posición (String, tamaño máximo 50).
  • abreviatura: abreviatura de la posición (String, tamaño máximo 3).
  • descripcion: descripción de la posición (String, tamaño máximo 255).

Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una colección de Posicion y el nombre de la tabla de unión sea JugadorPosicion.

Crea posiciones y añádelas a los jugadores que has creado en el ejercicio anterior.

Ejercicio 6.4. Mapeo de una base de datos de juegos

Migración de base de datos H2 entre versiones

En la base de datos origen se ejecuta el siguiente script:

SCRIPT TO '<ruta-al-archivo-backup>/backup.sql';

En la base de datos destino se ejecuta el siguiente script:

RUNSCRIPT FROM '<ruta-al-archivo-backup>/backup.sql';
Ejercicio 6.4. Mapeo de una base de datos de juegos.

Disponemos de una base de datos de juegos, que se compone de las siguientes tablas (la base de datos compartida está en el fichero anexo)

Plataforma: idPlataforma, nombre. (Ya contiene datos) Genero: idGenero, nombre. (Ya contiene datos) Juego: idJuego, idGenero (FK), idPlataforma (FK), titulo, miniatura (varchar), estado, descripciónCorta, descripcion, url, editor, desarrollador, fecha. Imagen: idImagen, idJuego (FK), url, imagen (tipo BLOB). RequisitosSistema: idJuego (PK), almacenamiento, graficos, memoria, os, procesador.

Referencias: https://www.freetogame.com/api-doc

  • Las plataformas pueden ser: pc, browser, all, etc. (Ya disponibles en la tabla Plataforma)
  • Las categorías (géneros) pueden ser:
    • mmorpg, shooter, strategy, moba, racing, sports, social, sandbox, open-world, survival, pvp, pve, pixel, voxel, zombie, turn-based, first-person, third-Person, top-down, tank, space, sailing, side-scroller, superhero, permadeath, card, battle-royale, mmo, mmofps, mmotps, 3d, 2d, anime, fantasy, sci-fi, fighting, action-rpg, action, military, martial-arts, flight, low-spec, tower-defense, horror, mmorts, etc. (Ya incorporadas en la tabla Genero)

Cuyos datos se ajustan al formato del siguiente JSON (ejemplo). Debes tener en cuenta que no se ha creado la tabla de requeriminetos mínimos, pero se puede hacer si se desea en una nueva tabla de la base de datos, relacionada, uno a uno:

{
    "id": 452,
    "title": "Call Of Duty: Warzone",
    "thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
    "status": "Live",
    "short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
    "description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
    "game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
    "genre": "Shooter",
    "platform": "Windows",
    "publisher": "Activision",
    "developer": "Infinity Ward",
    "release_date": "2020-03-10",
    "freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
    "minimum_system_requirements": {
        "os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
        "processor": "Intel Core i3-4340 or AMD FX-6300",
        "memory": "8GB RAM",
        "graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
        "storage": "175GB HD space"
    },
    "screenshots": [
        {
            "id": 1124,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg"
        },
        {
            "id": 1125,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg"
        },
        {
            "id": 1126,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg"
        },
        {
            "id": 1127,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg"
        }
    ]
}

a) Crea entidades JPA en Java para las tablas de la base de datos, con las siguientes características:

  • Genero: con los atributos idGenero, nombre. La clave es autonumérica.
  • Plataforma: con los atributos idPlataforma y nombre. La clave es autonumérica. Nota: si se hubiese declarado como enumeración, para poder mapear una enumeración en una tabla independiente, obligaría a crear una entidad independiente con el idPlataforma y el nombre. Sin embargo, en este caso, se podría mapear la enumeración directamente en la tabla Juego o declararla como una clase y no como una enumeración.
  • Juego: con todos los atributos de la tabla Juego, incluyendo la relación con Genero y Plataforma. La clave primaria, idJuego, no es autogenerada, es asignada. Ten en cuenta que la relación con la tabla Imagen se trata de una relación uno a muchos, por lo que se deberá declarar una colección de imágenes. Además, el idGenero y el idPlataforma son claves foráneas de las entidades y no deben declararse como atributos de la entidad Juego, sino como objetos del tipo de las entidades Genero y Plataforma.
  • Imagen: con los atributos idImagen (no autogenerada), Juego (relacionada con la entidad Juego @OneToOne), url, imagen (tipo byte[]).
  • RequisitosSistema: relacionada con la tabla Juego. Atributos: idJuego (PK), sistemaOperativo (su nombre no coincide con la columna de la tabla), almacenamiento, graficos, memoria, procesador y su relación juego. Debe emplearse una clave comparta con el idJuego. Para ello debe emplearse la anotación: @MapsId: @MapsId("idJuego").
  @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "idJuego")
  private Juego juego;

b) Haz una sencilla aplicación que cree un juego y lo persista en la base de datos. Ten en cuenta que las claves no son autonuméricas

{"status":0,"status_message":"No game found with that id"}
Bases de datos

6. Claves compartidas en relaciones uno a uno

Una clave compartida es una clave primaria que se comparte entre dos o más entidades. Una clave compartida se puede mapear con la anotación @MapsId y se emplea para relaciones uno a uno.

En este caso el identificador de un solo atributo es la clave foránea de la relación.

Por ejemplo, en una relación bidireccional uno a uno entre las entidades Empleado y HistorialEmpleado. Dado que solo hay un HistorialEmpleado por Empleado, podríamos decidir compartir la clave primaria (la clave primaria de HistorialEmplado sería la misma que Empleado).

Si HistorialEmpleado es la entidad dependiente, indicamos que la clave foránea de la relación es el identificador anotando la relación con @Id y @OneToOne. (En realidad suele escribirse la anotación @MapsId se coloca en el atributo de relación para indicar que también está mapeando el atributo de ID).

@Entity
public class HistorialEmpleado {
    // ...
    @Id
    @OneToOne
    @JoinColumn(name="idEmpleado")
    private Empleado empleado;
    // ...
}

El tipo de clave primaria de HistorialEmpleado va a ser del mismo tipo que Empleado, por lo que si Empleado tiene un identificador simple de tipo entero, entonces el identificador de HistorialEmpleado también será un entero.

Si Empleado tiene una clave primaria compuesta, ya sea con una clase ID o una clase ID incrustada, entonces HistorialEmpleado compartirá la misma clase ID (y también debería estar anotada con la anotación @IdClass).

El problema es que esto choca con la regla de la clase ID que dice que debe haber un atributo coincidente en la entidad por cada atributo en su clase ID. Esta es la excepción a la regla, debido al hecho mismo de que la clase ID se comparte entre ambas entidades, principal y dependiente.

6.1. Claves compartidas con @MapsId

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/mapsid

Generalmente, también se podría desear que la entidad contenga un atributo de clave primaria además del atributo de relación, con ambos atributos mapeados a la misma columna de clave foránea en la tabla.

Aunque el atributo de clave primaria es innecesario en la entidad podría querer definirse por separado para un acceso más fácil. A pesar de que los dos atributos se mapean a la misma columna de clave foránea (que también es la columna de clave primaria), el mapeo no tiene que duplicarse en ambos lugares. La anotación @Id se coloca en el atributo de identificación, y @MapsId anota el atributo de relación para indicar que también está mapeando el atributo de ID:

@Entity
public class HistorialEmpleado {
    // ...
    @Id
    int idEmpleado;
    
    @MapsId // Indica que el atributo de relación también mapea el atributo de ID
    @OneToOne
    @JoinColumn(name="idEmpleado")
    private Empleado empleado;
    // ...
}

Hay un par de puntos adicionales que vale la pena mencionar sobre @MapsId:

  • La relación anotada con @MapsId define el mapeo para el atributo de identificación también. Si no hay una anotación @JoinColumn que anule en el atributo de relación, entonces la columna de unión se asignará por defecto (nombreEnidad_idEntidad). En el ejemplo anterior, si se eliminara la anotación @JoinColumn, tanto el atributo empleado como el idEmpleado se mapearían a la columna de clave foránea predeterminada Empleado_idEmpleado (suponiendo que la columna de clave primaria en la tabla Empleado fuera idEmpleado).

  • Aunque el atributo de identificación comparte el mapeo de la base de datos definido en el atributo de relación, desde la perspectiva del atributo de identificación, es realmente un mapeo de solo lectura. Las actualizaciones o inserciones en la columna de clave foránea de la base de datos solo ocurrirán a través del atributo de relación. Esta es una de las razones por las que siempre se debe establecer las relaciones padre antes de intentar persistir una entidad dependiente (Debes persistir, por ejemplo, primero Empleado y luego HistorialEmpleado).

IMPORTANTE: Claves compartidas

Nota: No intentes establecer solo el atributo de identificación (y no el atributo de relación) como un medio para atajar la persistencia de una entidad dependiente. Algunos proveedores pueden tener soporte especial para hacer esto, pero no garantizará de manera portátil que la clave foránea se escriba en la base de datos. El atributo de identificación se completará automáticamente por el proveedor cuando se lea una instancia de entidad de la base de datos o cuando se realiza un flush/commit. Sin embargo, no se puede asumir que esté presente al llamar primero a persist() en una instancia a menos que el usuario lo establezca explícitamente.

6.2. PrimaryKeyJoinColumn y PrimaryKeyJoinColumns

La anotación @PrimaryKeyJoinColumn se utiliza para especificar una columna de clave primaria de una tabla de unión. Esto es, cuando la clave foránea es la clave primaria de la tabla de unión. Se usa en relaciones uno a uno y solo se puede usar en la entidad propietaria de la relación.

@PrimaryKeyJoinColumns se utiliza para especificar varias columnas de clave primaria de una tabla de unión.

Por ejemplo:

    @Entity
    public class Empleado {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int idEmpleado;
        private String nombre;
        private long salario;
        
        @OneToOne
        @PrimaryKeyJoinColumn // Indica que la columna de clave primaria de la tabla de unión es la misma que la clave primaria de la tabla de la entidad
        private HistorialEmpleado historial;
        // ...
    }

En este caso, la clave primaria de la tabla HistorialEmpleado sería la clave foránea de la tabla Empleado.

    @Entity
    public class HistorialEmpleado {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private int id;
        private String nombre;
        private long salario;
        // ...
    }

En el segundo código, la entidad HistorialEmpleado tiene un campo @Id que actúa como clave primaria de la tabla, por lo que no tiene sentido usar @PrimaryKeyJoinColumn en este caso. La entidad HistorialEmpleado tiene un campo @Id que actúa como clave primaria de la tabla.

Ejercicio 6.5. Claves compartidas en relaciones uno a uno

Comprueba el funcionamiento de la anotación @PrimaryKeyJoinColumn en una relación uno a uno entre Persona y Departamento. Crea las entidades y realiza pruebas de persistencia.

Persona: idPersona (IDENTITY), nombre, departamento (uno a uno con anotación de @PrimaryKeyJoinColumn) Departamento: idDepartamento (IDENTITY), nombre.

Modifica el ejercicio para que sea bidireccional con @OneToOne y @MapsId en la entidad Departamento y como propietaria de la relación.

Última actualización: 23.09.2025

07. Objetos embebidos.

1. Objetos embebidos: @Embeddable

Un objeto embebido es un objeto que no tiene identidad propia y que es parte de una entidad.

Los objetos embebidos se utilizan para modelar datos compuestos.

Es parte del estado de una entidad que ha sido extraído y modelado como un objeto independiente.

En Java los objetos embebidos se modelan como clases normales y se anotan con @Embeddable. Sin embargo, en la base de datos, los objetos embebidos se almacenan en la misma tabla que la entidad que los contiene, como cualquier otro de sus atributos.

@Embeddable y @Embedded

La anotación @Embeddable se utiliza para indicar que una clase es un objeto embebido. La anotación @Embedded se utiliza para indicar que un atributo de una entidad es un objeto embebido.

Aunque los objeto embebidos son referenciados por entidades, no se consideran relaciones entre entidades.

Aunque pudiera parecer contradictorio separar un objeto de una entidad y luego volver a unirlo, hay varias razones por las que esto es útil:

  • Los objetos embebidos pueden ser reutilizados. Si tienes un objeto embebido que representa una dirección, por ejemplo, puedes reutilizarlo en cualquier entidad que necesite una dirección.

Aunque los tipos embebidos pueden ser compartidos o reutilizados, las instancias no. Una instancia de objeto embebido pertenece a la entidad que la referencia; y ninguna otra instancia de entidad, de ese tipo de entidad o de cualquier otro, puede hacer referencia a la misma instancia embebida.

Un ejemplo de reutilización es la información de la dirección postal:

classDiagram
    class Direccion {
        -String calle
        -String ciudad
        -String provincia
        -String codigoPostal
    }
    class Empleado {
        -long idEmpleado
        -String nombre
        -long salario
        -Direccion direccion
    }
    class Compañia {
        -String nombre
        -Direccion direccion
    }
    Empleado o-- Direccion
    Compañia o-- Direccion

La tabla Empleado:

erDiagram
    Empleado {
        idEmpleado int
        nombre varchar
        salario int
        calle varchar
        ciudad varchar
        provincia varchar
        codigoPostal varchar
    }
idEmpleado nombre salario calle ciudad provincia codigoPostal
1 Pepe 1500 C/1 Santiago A Coruña 15706
2 Xan 2200 C/2 Vigo Pontevedra 36201

Una tabla Empleado que contiene una mezcla de información básica del empleado, así como columnas que corresponden a la dirección postal del empleado. Las columnas calle, ciudad, provincia y codigoPostal se combinan lógicamente para formar la dirección (clase Direccion).

En el modelo de objetos, es una excelente candidata para ser “abstracta” en un tipo embebido Direccion en lugar de incorporar cada atributo en la clase de la entidad. La clase de entidad solo tendría un atributo de dirección que apunta a un objeto embebido de tipo Address. La figura muestra cómo Empleado y Direccion se relacionan entre sí.

@Embeddable
@Access(AccessType.FIELD)
public class Direccion {
    private String calle;
    private String ciudad;
    private String provincia;
    @Column(name="codigoPostal")
    private String codigo;
    // ...
}

@Entity
public class Empleado {
    @Id private long idEmpleado;
    private String nombre;
    private long salario;
    @Embedded
    private Direccion direccion;
    // ...
}

Al persistir una instancia de Empleado, se accede a los atributos del objeto Direccion como si estuvieran presentes en Empleado.

Las asignaciones de columnas en el tipo Direccion realmente se refieren a las columnas de la tabla Empleado, aunque estén en una clase separada.

Objetos embebidos o entidades

Es una decisión de diseño el uso de objetos embebidos. Si se precisa crear relaciones con ellos o desde ellos, no los uses. Los objetos embebidos no están destinados a ser entidades y tan pronto como comiences a tratarlos como entidades, probablemente deberías convertirlos en entidades de primera clase si el modelo de datos lo permite.

No es portátil definir objetos embebidos como parte de jerarquías de herencia. Una vez que comienzan a heredarse entre sí, la complejidad de su incorporación aumenta y la relación costo-beneficio disminuye.

Una clase Direccion podría ser reutilizada tanto en las entidades Empleado como Companía (como hemos indicado en la imagen anterior).

Empleado-Dirección-Compañía Empleado-Dirección-Compañía

Aunque tanto las clases Empleado como Compañía contiene la clase Direccion, cada instancia de Direccion será utilizada solo por una única instancia de Empleado o Compañía.

2. Sustitución de atributos embebidos: @AttributeOverride

@AttributeOverride

Como las asignaciones de columnas del tipo embebido Direccion se aplican a las columnas de la entidad contenedora (en tablas diferentes), las tablas de entidades podrían tener nombres de columna diferentes para los mismos campos.

erDiagram
    Empleado {
        idEmpleado int
        nombre varchar
        salario int
        calle varchar
        ciudad varchar
        provincia varchar
        codigoPostal varchar
    }
    Compania {
        nombre varchar
        calle varchar
        ciudad varchar
        prov varchar
        codPostal varchar
    }

La tabla Empleado coincide con los atributos predeterminados y mapeados del tipo Direccion, pero la tabla Compania se ha modificado con otros nombres de provincia y código postal:

@Entity
public class Empleado {
    @Id private long idEmpleado;
    private String nombre;
    private long salario;
    @Embedded
    private Direccion direccion;
    // ...
}

@Entity
public class Compania {
    @Id private String name;
    @Embedded
    @AttributeOverride(name = "provincia", column = @Column(name = "prov")),
    @AttributeOverride(name = "codigoPostal", column = @Column(name = "codPostal"))
    private Direccion direccion;
    // ...
}

Como se muestra en el ejemplo, para los cambios de nombres en los atributos se puede emplear la anotación @AttributeOverride.

En la declaración de la entidad se emplea la anotación @AttributeOverride para cada atributo del objeto embebido que queremos renombrar en la entidad. Elementos requeridos:

  • name: el nombre del campo o propiedad embebido en la entidad.
  • column: la columna a la que se está asignando el atributo en la tabla de la entidad. Se especifica en forma de una anotación @Column anidada.

2.1. Sustitución de múltiples atributos embebidos: @AttributeOverrides

Si sobrescribimos múltiples campos o propiedades, se puede usar la anotación plural @AttributeOverrides y anidar múltiples anotaciones @AttributeOverride dentro de ella:

@Entity
public class Compania {
    @Id private String name;
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "provincia", column = @Column(name = "prov")),
        @AttributeOverride(name = "codigoPostal", column = @Column(name = "codPostal"))
    })
    private Direccion direccion;
    // ...
}

Dado que la anotación @AttributeOverride puede repetirse, no es obligatorio el uso de la anotación @AttributeOverrides. El siguiente ejemplo muestra el uso de Direccion tanto en Empleado como en Compañía. La entidad Empleado utiliza el tipo Direccion sin cambios, pero la entidad Compañía sobrescribe para asignar los atributos provincia y codigoPosta de Direccion a las columnas prov y codPostal de la tabla Companía.

@Entity
public class Empleado {
    @Id private long idEmpleado;
    private String nombre;
    private long salario;
    @Embedded
    private Direccion direccion;
    // ...
}

@Entity
public class Companía {
    @Id private String name;
    @Embedded
    @AttributeOverride(name = "provincia", column = @Column(name = "prov")),
    @AttributeOverride(name = "codigoPostal", column=@Column(name = "codPostal"))
    private Direccion direccion;
    // ...
}

Ejercicio 7.1. Elementos embebidos.

Crea una aplicación con JPA para la gestión de películas y series.

  1. Crea una clase InfoContenido con los siguientes atributos:

    • titulo (String): de tamaño 100.
    • genero (String): de tamaño 50.
    • pais (String): de tamaño 2.
    • duracion (int): duración en minutos.
    • año (int): año.
    • sinopsis (String): de tamaño clob.
  2. Crea una entidad Serie con los siguientes atributos:

    • idSerie (long): identificador de la serie. Secuencia.
    • informacion (de tipo InfoContenido)
    • fechaEstreno (LocalDate).
    • temporadas (int): número de temporadas.
    • capitulos (int)
    • directores (lista de String).
  3. Crea una entidad Pelicula con los siguientes atributos:

    • idPelicula (long): identificador de la película. Secuencia.
    • informacion (de tipo InfoContenido)
  • La entidad Serie y Pelicula deben tener el atributo informacion como un objeto embebido.

  • La entidad Pelicula el atributo pais debe ser renombrado a paisPelicula.

  • El atributo directores debe guardarse en una nueva tabla, como una colección con la anotación @ElementCollection (busca información sobre esta anotación).

  • La fecha de estreno, fechaEstreno, de la serie debe guardarse en formato numérico (YYYYMMDD).

4. ManyToMany usando una clave compuesta

A modo de ejemplo, tenemos una relación de muchos a muchos entre dos entidades, Estudiante y Curso. Un estudiante puede inscribirse en varios cursos, y un curso puede tener varios estudiantes inscritos. Además, queremos que los estudiantes califiquen los cursos, que sería un atributo de la relación. Un estudiante puede calificar cualquier número de cursos, y cualquier número de estudiantes puede calificar el mismo curso.

Una clave primaria compuesta, también llamada clave compuesta, es una combinación de dos o más columnas para formar una clave primaria para una tabla.

En JPA, tenemos dos opciones para definir las claves compuestas: las anotaciones @IdClass y @EmbeddedId.

Para definir las claves primarias compuestas, debemos seguir algunas reglas:

  • La clase de clave primaria compuesta debe ser pública.
  • Debe tener un constructor sin argumentos.
  • Debe definir los métodos equals() y hashCode().
  • Debe ser Serializable.

4.1. Modelando Atributos de Relación

Queremos permitir que los estudiantes califiquen los cursos. Un estudiante puede calificar cualquier número de cursos, y cualquier número de estudiantes puede calificar el mismo curso. Por lo tanto, también es una relación de muchos a muchos.

classDiagram
    Estudiante -- CalificacionCurso
    CalificacionCurso -- Curso
    Estudiante : idEstudiante
    Curso : idCurso
    CalificacionCurso : calificacion

A diferencia de las otras relaciones muchos a muchos, necesitamos almacenar la puntuación de calificación que el estudiante dio al curso, en la tabla intermedia.

¿Dónde podemos almacenar esta información? No podemos ponerla en la entidad Estudiante ya que un estudiante puede dar diferentes calificaciones a diferentes cursos. De manera similar, almacenarlo en la entidad Curso tampoco sería una buena solución.

Esta es una situación en la que la relación en sí tiene un atributo.

Usando este ejemplo, adjuntar un atributo a una relación se ve así en un diagrama ER:

classDiagram
    Estudiante -- CalificacionCurso
    CalificacionCurso -- Curso
    Estudiante : idEstudiante
    Curso : idCurso
    CalificacionCurso : calificacion

Podemos modelarlo casi de la misma manera que la relación de muchos a muchos sencilla. La única diferencia es que añadimos un nuevo atributo a la tabla de unión:

Estudiante - CalificacionCurso - Curso Estudiante - CalificacionCurso - Curso

4.2. Creando una Clave Compuesta en JPA: @Embeddable

La implementación de una relación de muchos a muchos sencilla fue bastante directa, pero no podemos agregar una propiedad a una relación de esa manera porque conectamos las entidades directamente. Por lo tanto, no teníamos forma de añadir una propiedad a la relación en sí.

Dado que mapeamos los atributos de la base de datos a campos de clase en JPA, necesitamos crear una nueva clase de entidad para la relación:

Estudiante - CalificacionCurso - Curso Estudiante - CalificacionCurso - Curso

Cada entidad JPA necesita una clave primaria. Dado que la clave primaria es una clave compuesta, tenemos que crear una nueva clase para la clave, ClaveCalificacionCurso, que contendrá las diferentes partes de la clave:

@Embeddable
class ClaveCalificacionCurso implements Serializable {

    @Column(name = "idEstudiante")
    private Long idEstudiante;

    @Column(name = "idCurso")
    private Long idCurso;

    // constructores estándar, getters y setters
    // implementación de hashcode y equals
}

Ten en cuenta que una clase de clave compuesta debe cumplir con algunos requisitos clave:

  • Debe marcarse con @Embeddable.
  • Debe implementar java.io.Serializable.
  • Necesitamos proporcionar una implementación de los métodos hashcode() e equals().

4.3. Utilizando una Clave Compuesta en JPA. @EmbeddedId

Usando esta clase de clave compuesta, podemos crear la clase de entidad que modela la tabla de unión:

@Entity
class CalificacionCurso {

    @EmbeddedId
    private ClaveCalificacionCurso id;

    @ManyToOne
    @MapsId("idEstudiante")
    @JoinColumn(name = "idEstudiante")
    private Estudiante estudiante;

    @ManyToOne
    @MapsId("idCurso")
    @JoinColumn(name = "idCurso")
    private Curso curso;

    int calificacion;
    
    // constructores estándar, getters y setters
}

Este código es muy similar a una implementación usual de entidad. Sin embargo, tenemos algunas diferencias clave:

  • Usamos @EmbeddedId para marcar la clave primaria, que es una instancia de la clase ClaveCalificacionCurso (recuerda que antes añadimos la anotación @Embeddable a esta clase).
  • Marcamos los campos estudiante y curso con @MapsId.
  • @MapsId significa que vinculamos esos campos a una parte de la clave y son las claves externas de una relación de muchos a uno.

Después de esto, podemos configurar las referencias inversas en las entidades Estudiante y Curso como antes:

class Estudiante {

    // ...

    @OneToMany(mappedBy = "estudiante")
    Set<CalificacionCurso> calificaciones;

    // ...
}

class Curso {

    // ...

    @OneToMany(mappedBy = "curso")
    Set<CalificacionCurso> calificaciones;

    // ...
}

Ten en cuenta que hay una forma alternativa de usar claves compuestas: la anotación @IdClass.

4.4. Características Adicionales

Configuramos las relaciones con las clases Estudiante y Curso como @ManyToOne, porque con la nueva entidad descompusimos estructuralmente la relación de muchos a muchos en dos relaciones de muchos a uno.

Ahora tenemos dos relaciones de muchos a uno. En otras palabras, no hay ninguna relación de muchos a muchos en un RDBMS. Llamamos a las estructuras que creamos con tablas de unión relaciones de muchos a muchos porque eso es lo que modelamos.

Además, es más claro si hablamos de relaciones de muchos a muchos porque esa es nuestra intención. Mientras tanto, una tabla de unión es solo un detalle de implementación; realmente no nos importa.

Esta solución tiene una característica adicional que aún no hemos mencionado. La solución simple de muchos a muchos crea una relación entre dos entidades. Por lo tanto, no podemos expandir la relación a más entidades. Pero no tenemos este límite en esta solución: podemos modelar relaciones entre cualquier número de tipos de entidades.

Cuando varios profesores pueden enseñar un curso, los estudiantes pueden calificar cómo un profesor específico enseña un curso específico. De esa manera, una calificación sería una relación entre tres entidades: un estudiante, un curso y un profesor.

Ejercicio 7.2. Clave compuesta en una relación de muchos a muchos.

Data la aplicación de gestión de películas y series, añade dos nuevas entidades: Usuario y Calificacion que permita a los usuarios calificar las películas.

  1. Crea una clase Usuario con los siguientes atributos:

    • idUsuario (long): identificador del usuario. Secuencia.
    • nombre (String): nombre del usuario.
    • email (String): email del usuario.
    • password (String): contraseña del usuario.
    • fechaRegistro (LocalDate): fecha de registro.
  2. Crea una clase Calificacion con los siguientes atributos:

    • calificacion (int): calificación del contenido, con valores de 10 a 0.
    • fechaCalificacion (LocalDate): fecha de la calificación.
    • comentario (String): comentario de la calificación.
    • Además, debe estar relacionado con las entidades Usuario, Pelicula y Serie. Como un usuario puede calificar varias películas y series, y una película o serie puede ser calificada por varios usuarios, es una relación de muchos a muchos. No es preciso que califique series, pues el caso de uso es similar al de las películas.

La clave primaria de la tabla Calificacion debe ser compuesta por los atributos idUsuario, idPelicula.


4.5. La anotación @IdClass

Digamos que tenemos una tabla llamada Cuenta y tiene dos columnas, numeroCuenta y tipoCuenta, que forman la clave compuesta. Ahora tenemos que mapearlo en JPA.

Según la especificación de JPA, creemos una clase IdCuenta con estos campos de clave primaria:

public class IdCuenta implements Serializable {
    private String numeroCuenta;
    private String tipoCuenta;

    // constructor por defecto

    public IdCuenta(String numeroCuenta, String tipoCuenta) {
        this.numeroCuenta = numeroCuenta;
        this.tipoCuenta = tipoCuenta;
    }

    // métodos equals() y hashCode()
}

A continuación, asociemos la clase IdCuenta con la entidad Cuenta.

Para hacer eso, necesitamos anotar la entidad con la anotación @IdClass. También debemos declarar los campos de la clase IdCuenta en la entidad Cuenta y anotarlos con @Id:

@Entity
@IdClass(IdCuenta.class)
public class Cuenta {
    @Id
    private String numeroCuenta;

    @Id
    private String tipoCuenta;

    // otros campos, getters y setters
}

Mapeando el estado de la relación

En ocasiones, una relación tiene un estado asociado. Por ejemplo, supongamos que queremos mantener la fecha en la que un empleado fue asignado a trabajar en un proyecto (atributo de la relación). Almacenar el estado en el empleado es posible, pero menos útil, ya que la fecha está realmente asociada a la relación del empleado con un proyecto particular (una sola entrada en la asociación de muchos a muchos). Sacar a un empleado de un proyecto debería hacer que la fecha de asignación desaparezca, por lo que almacenarla como parte del empleado significa que tenemos que asegurarnos de que ambos sean consistentes entre sí, lo cual puede ser molesto. En UML, mostraríamos este tipo de relación usando una clase de asociación. El siguiente esquema muestra un ejemplo de esta técnica, en la que Employee tiene un id (Long), un nombre (string) y una salario (long), un proyecto tiene un id (Long) y un nombre (string), y la relación entre ellos tiene una fecha de inicio (Date).

classDiagram
    class Empleado {
        -Long id
        -String nombre
        -long salario
    }
    class Proyecto {
        -Long id
        -String nombre
    }
    class AsignacionProyecto {
        -Date fechaInicio
    }
    Empleado o-- AsignacionProyecto
    Proyecto o-- AsignacionProyecto

In the database, everything is rosy because we can simply add a column to the join table. The data model provides natural support for relationship state. Figure 10-7 shows the many-to-many relationship between EMPLOYEE and PROJECT with an expanded join table.

En la base de datos, todo es perfecto porque podemos simplemente añadir una columna a la tabla de unión. El modelo de datos proporciona un soporte natural para el estado de la relación. La figura anterior muestra la relación muchos a muchos entre EMPLEADO y PROYECTO con una tabla de unión expandida.

erDiagram
    EMPLEADO {
        id int
        nombre varchar
        salario int
    }
    PROYECTO {
        id int
        nombre varchar
    }
    ASIGNACION_PROYECTO {
        EMP_ID int
        PROJECT_ID int
        START_DATE date
    }

Cuando llegamos al modelo de objetos, sin embargo, se vuelve mucho más problemático. El problema es que Java no tiene un soporte inherente para el estado de la relación. Las relaciones son solo referencias a objetos o punteros; por lo tanto, nunca puede existir estado en ellas (las relaciones no pueden contener atributos). El estado existe en los objetos solamente, y las relaciones no son objetos de primera clase.

La solución Java es convertir la relación en una entidad que contenga el estado deseado y mapear la nueva entidad a lo que anteriormente era la tabla de unión. La nueva entidad tendrá una relación de muchos a uno con cada uno de los tipos de entidad existentes, y cada uno de los tipos de entidad tendrá una relación de uno a muchos de vuelta a la nueva entidad que representa la relación.

La clave primaria de la nueva entidad será la combinación de las dos relaciones con los dos tipos de entidad. El siguiente código muestra todos los participantes en la relación entre Empleado y Proyecto.

classDiagram
    class Empleado {
        -int id
        -String nombre
        -long salario
    }
    class Proyecto {
        -int id
        -String nombre
    }
    class AsignacionProyecto {
        -Date fechaInicio
    }
    Empleado o-- AsignacionProyecto
    Proyecto o-- AsignacionProyecto
La relación entre Employee y Project

La relación entre Employee y Project es una relación de muchos a muchos, que se representa mediante una tabla de unión llamada EMP_PROJECT. Esta tabla contiene dos claves externas, EMP_ID y PROJECT_ID, que hacen referencia a las tablas EMPLOYEE y PROJECT, respectivamente. La clase ProjectAssignment representa la relación entre Employee y Project, y contiene un atributo adicional, startDate, que almacena la fecha en la que se realizó la asignación.

Mapeando el estado de la relación con una entidad intermedia:

public class Employee {
    @Id private int id;
    // ...
    @OneToMany(mappedBy="employee")
    private Collection<ProjectAssignment> assignments;
    // ...
}

public class Project {
    @Id private int id;
    // ...
    @OneToMany(mappedBy="project")
    private Collection<ProjectAssignment> assignments;
    // ...
}

@Entity
@Table(name="EMP_PROJECT")
@IdClass(ProjectAssignmentId.class)
public class ProjectAssignment {
    @Id
    @ManyToOne
    @JoinColumn(name="EMP_ID")
    private Employee employee;
    @Id
    @ManyToOne
    @JoinColumn(name="PROJECT_ID")
    private Project project;
    @Temporal(TemporalType.DATE)
    @Column(name="START_DATE", updatable=false)
    private Date startDate;
    // ...
}

public class ProjectAssignmentId implements Serializable {
    private int employee;
    private int project;
    // ...
}

Aquí tenemos una clave primaria compuesta que está compuesta por dos claves externas, una de cada una de las entidades que forman parte de la relación. La clase ProjectAssignmentId es la clave primaria compuesta y contiene los dos atributos que forman la clave primaria. La clase ProjectAssignment es la entidad que representa la relación entre Employee y Project. La relación entre Employee y Project se representa mediante una relación de muchos a uno con la entidad ProjectAssignment.

Nombre de los atributos de la IdClass

Los nombres de los atributos de la clase IdClass deben coincidir con los nombres de los atributos de la clase de la entidad que representa la relación. En este caso, los nombres de los atributos son employee y project.

Ojo, los tipos de datos deben coincidir con los tipos de datos de las claves de las entidades. En este caso, ambos atributos son de tipo int, que coincide con los tipos de datos de las claves de las entidades.

La clave primaria enteramente compuesta de la relación, con las dos columnas de clave externa que componen la clave primaria en la tabla de unión EMP_PROYECTO. La fecha en la que se realizó la asignación podría establecerse manualmente cuando se crea la asignación, o podría asociarse a un trigger que haga que se establezca cuando se crea la asignación en la base de datos. Ten en cuenta que, si se usara un trigger, entonces la entidad tendría que actualizarse desde la base de datos para poder poblar el campo de fecha de asignación en el objeto Java.

4.6 La anotación EmbeddedId (repaso)

@EmbeddedId es una alternativa a la anotación @IdClass.

Consideremos otro ejemplo en el que tenemos que persistir información de un Book, con titulo y idioma como los campos de clave primaria.

En este caso, la clase de clave primaria, IdLibro, debe estar anotada con @Embeddable:

@Embeddable
public class IdLibro implements Serializable {
    private String titulo;
    private String idioma;

    // constructor por defecto

    public IdLibro(String titulo, String idioma) {
        this.titulo = titulo;
        this.idioma = idioma;
    }

    // getters, métodos equals() y hashCode()
}

Luego, necesitamos incrustar esta clase en la entidad Libro usando @EmbeddedId:

@Entity
public class Libro {
    @EmbeddedId
    private IdLibro bookId;

    // constructores, otros campos, getters y setters
}

4.7. @IdClass vs @EmbeddedId

Como podemos ver, la diferencia superficial entre estos dos es que con @IdClass tuvimos que especificar las columnas dos veces, una vez en IdCuenta y nuevamente en Cuenta; sin embargo, con @EmbeddedId no lo hicimos.

Sin embargo, hay algunas otras compensaciones.

Por ejemplo, estas estructuras diferentes afectan las consultas JPQL que escribimos.

Con @IdClass, la consulta es un poco más sencilla:

SELECT cuenta.numeroCuenta FROM Cuenta cuenta

Con @EmbeddedId, tenemos que hacer un recorrido extra:

SELECT libro.idLibro.titulo FROM Libro book

Además, @IdClass puede ser bastante útil en lugares donde estamos utilizando una clase de clave compuesta que no podemos modificar.

Si vamos a acceder a partes de la clave compuesta individualmente, podemos hacer uso de @IdClass, pero en lugares donde usamos frecuentemente el identificador completo como un objeto, se prefiere @EmbeddedId.

Ejercicio 7.3. Entidades principales de base de datos de películas.

            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas"/>
            <property name="jakarta.persistence.jdbc.user" value="accesoadatos"/>
            <property name="jakarta.persistence.jdbc.password" value="ad123.."/>
            <property name="jakarta.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver"/>
            <property name="jakarta.persistence.schema-generation.database.action" value="none"/>

Sea la siguiente estructura de la base de datos:

Estructura de la base de datos Estructura de la base de datos

SQL de las tablas de la base de datos
CREATE TABLE IF NOT EXISTS `pelicula` (
   `idPelicula` int(10) NOT NULL,
   `musica` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `orixinal` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `ingles` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `castelan` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `xenero` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `anoInicio` smallint(5) DEFAULT NULL,
   `anoFin` smallint(5) DEFAULT NULL,
   `pais` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `duración` smallint(5) DEFAULT NULL,
   `outrasDuracions` varchar(25) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cor` varchar(12) COLLATE utf8_spanish_ci DEFAULT NULL,
   `son` varchar(6) COLLATE utf8_spanish_ci DEFAULT NULL,
   `video` varchar(2) COLLATE utf8_spanish_ci DEFAULT NULL,
   `laserDisc` varchar(2) COLLATE utf8_spanish_ci DEFAULT NULL,
   `texto` longtext COLLATE utf8_spanish_ci,
   `poster` longblob,
   `revisado` varchar(10) COLLATE utf8_spanish_ci DEFAULT NULL,
   PRIMARY KEY (`idPelicula`),
   UNIQUE KEY `Película#PX` (`idPelicula`),
   KEY `Género` (`xenero`),
   KEY `GéneroPelícula` (`xenero`),
   KEY `OriginalAnyo` (`orixinal`,`anoFin`),
   KEY `País` (`pais`),
   KEY `PaísPelícula` (`pais`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

CREATE TABLE IF NOT EXISTS `personaxe` (
   `idPersonaxe` int(10) NOT NULL,
   `importancia` varchar(16) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nome` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nomeOrdenado` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nomeOrixinal` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `sexo` varchar(6) COLLATE utf8_spanish_ci DEFAULT NULL,
   `dataNacemento` datetime DEFAULT NULL,
   `paisNacemento` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cidadeNacemento` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `dataDefuncion` datetime DEFAULT NULL,
   `paisDefuncion` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cidadeDefuncion` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `estudio` varchar(1) COLLATE utf8_spanish_ci DEFAULT NULL,
   `bio` varchar(1) COLLATE utf8_spanish_ci DEFAULT NULL,
   `texto` longtext COLLATE utf8_spanish_ci,
   `textoFilmografia` longtext COLLATE utf8_spanish_ci,
   `revisado` varchar(10) COLLATE utf8_spanish_ci DEFAULT NULL,
   PRIMARY KEY (`idPersonaxe`),
   UNIQUE KEY `Personaje#PX` (`idPersonaxe`),
   KEY `NomPersona` (`nome`),
   KEY `País de defunción` (`paisDefuncion`),
   KEY `País de nacimiento` (`paisNacemento`),
   KEY `PaísPersonaje` (`paisNacemento`),
   KEY `PaísPersonaje1` (`paisDefuncion`),
   KEY `SexoPersonaxe` (`sexo`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;


CREATE TABLE IF NOT EXISTS `peliculapersonaxe` (
`idPersonaxe` int(10) NOT NULL,
`idPelicula` int(10) NOT NULL,
`ocupacion` varchar(50) COLLATE utf8_spanish_ci NOT NULL,
`personaxeInterpretado` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
PRIMARY KEY (`idPelicula`,`idPersonaxe`,`ocupacion`),
KEY `OcupaciónPelícula_Personaje` (`ocupacion`),
KEY `PersonajePelícula_Personaje` (`idPersonaxe`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

En el que:

  • El título de la película se guarda en el campo castelan.
  • El identificador de la película es entero (no autoincremento).
  • Los participantes de la película están relacionados por medio da de la tabla peliculapersonaxe, en la que el campo ocupacion identifica o tipo de ocupación de la película (‘Actor’, …):
CREATE TABLE IF NOT EXISTS `ocupacion` (
   `ocupacion` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `orde` int(11) NOT NULL,
   UNIQUE KEY `Ocupación#PX` (`ocupacion`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

Ocupación Ocupación

Personaxeocupación Personaxeocupación

Para empezar, crea las entidades Pelicula, Personaxe y Ocupacion. A continuación, crea la entidad PeliculaPersonaxe que relaciona las entidades Pelicula y Personaxe y Ocupacion. Ten en cuenta que tiene un nuevo atributo personaxeInterpretado, que es el nombre del personaje interpretado por el actor en la película.

Mejora:

Crea las entidades asociadas a la base de datos. De modo que las columnas anoInicio, outrasDuracions, video, laserDisc pertenezca a una entidad DetallePelicula de tipo embebido. Hay que tener en cuenta que estos elementos no siempre están presentes, por lo que deben ser opcionales.


Última actualización: 23.09.2025

08. Mapeo de colecciones.

1. Relaciones y colecciones de elementos (@ElementCollection)

Cuando hablamos de mapear colecciones hay tres tipos de objetos que pueden contener colecciones:

  • Entidades (@OneToMany o @ManyToMany).
  • Elementos embebidos (@ElementCollection)
  • Elementos básicos (@ElementCollection)

Sin embargo, las colecciones de elementos embebidos y básicos no se consideran relaciones. Son colecciones de elementos que se denominan colecciones de elementos, pues, a diferencia de las relaciones, que se relacionan con entidades independientes, las colecciones de elementos contienen objetos que dependen de la entidad que los referencia.

Sólo pueden ser obtenidos a través de la entidad que los contiene.

La anotación @ElementCollection se utiliza para mapear colecciones de elementos embebidos o básicos. Dispones de dos elementos opcionales:

  • targetClass: de tipo Class indica la clase básica o embebida del elemento de la colección. Es opcional si el tipo de elementos de la colección se indica con genéricos, obligatorio en caso contrario.

  • fetch: de tipo FechType tipo de carga de los elementos de la colección, perezosa o ansiosa. Por defecto es perezosa.

En el siguiente ejemplo se requiere indicar la clase de los elementos de la colección con el atributo targetClass, pues es una colección sin genéricos (perezosa es el valor por defecto):

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection(targetClass=Vacaciones.class, fetch=FetchType.LAZY)
        private Collection vacaciones;
        // ...
    }

Por ejemplo con lista de cadenas de texto con genéricos (ambos atributos son opcionales):

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection
        private List<String> telefonos;
        // ...
    }

En este caso, la colección de teléfonos es una colección de elementos básicos. No es una relación, pues los teléfonos no son entidades independientes, sino que dependen de la persona que los referencia.

También podría ser un conjunto (Set) de elementos:

    @Entity public class Persona {
       @Id
       protected String numeroSeguridadSocial;
       protected String nombre;
       // ...
       @ElementCollection  
       protected Set<String> apodos = new HashSet();
       //  ...
    } 

O una colección de elementos embebidos:

    @Embeddable
    public class Vacaciones {
        @Temporal(TemporalType.DATE)
        private Calendar fechaInicio;
        
        @Column(name="duracionDias")
        private int duracion;
        // ...
    }

    @Entity 
    public class Persona {
        @Id
        private long idPersona;
        private String nombre;
        @ElementCollection(targerClass=Vacaciones.class, fetch=FetchType.LAZY)
        private Collection vacaciones;
        @ElementCollection
        protected Set<String> apodos = new HashSet();
        // ...
    }

En el ejemplo anterior, la anotación @ElementCollection incluye el atributo targetClass que indica la clase de los elementos de la colección porque no se ha indicado el tipo de elementos que contiene la colección.

2. Tabla de colección: @CollectionTable

La anotación @CollectionTable se utiliza para especificar la tabla de la colección.

En el caso de las colecciones de elementos embebidos, los elementos no son entidades, por lo que no tendrá asociada una tabla, sin embargo, al no ser posible almacenar varios elementos en la misma columna, las colecciones de elementos embebidos y tipos básicos (algunos sistemas gestores de base de datos disponen de tipo de datos para ARRAY) requieren una tabla independiente denominada “Tabla de colección” que almacena los elementos de la colección.

Es posible especificar la tabla de la colección con la anotación @CollectionTable:

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection(targerClass=Vacaciones.class, fetch=FetchType.LAZY)
        @CollectionTable(name="Vacaciones", joinColumns=@JoinColumn(name="idPersona"))
        private Collection vacaciones;
        // ...
    }

En este caso, la tabla Vacaciones almacenará los elementos de la colección vacaciones de la entidad Persona. La tabla Vacaciones tendrá una columna idPersona que será la clave foránea que relacionará los elementos de la colección con la entidad Persona.

Si no se especifica la tabla de la colección, se utilizará una tabla por defecto con el nombre de la entidad y el nombre de la propiedad de la colección separados por un guion bajo. Por ejemplo, si no se especifica la tabla de la colección, la tabla por defecto de la colección vacaciones de la entidad Persona se llamará Persona_Vacaciones.

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection(targerClass=Vacaciones.class, fetch=FetchType.LAZY)
        private Collection vacaciones;
        // ...
    }

2.1 Columnas de colección: @Column

Por defecto, las columnas de la tabla de la colección tendrán el mismo nombre que la propiedad de la colección. Sin embargo, se pueden especificar las columnas de la tabla de colección con la anotación @Column.

  • Con colecciones de tipo básico, el nombre de a columna se obtiene de nombre de campo o propiedad de la colección.
    • Se puede sobrescribir con la anotación @Column indicando el nombre de la columna.
@ElementCollection // usa la tabla por defecto: Persona_telefonos
@Column(name="telefono")
private List<String> telefonos;
  • Con colecciones de clases embebidas los nombres de las columnas se corresponden con los de las propiedades de la clase embebida.
    • Se puedes sobrescribir con la anotación @AttributeOverride o @AttributeOverrides indicando el nombre de la columna (si tiene referencias a otras entidades puede sobrescribirse con @AssociationOverride o @AssociationOverrides).
@ElementCollection
@CollectionTable(name="Residencia")
@AttributeOverrides({
    @AttributeOverride(name="calle", column=@Column(name="calleCasa")), @AttributeOverride(name="ciudad", column=@Column(name="ciudadCasa")), @AttributeOverride(name="provincia", column=@Column(name="provinciaCasa"))
    })
private Set<Direccion> direcciones = new HashSet();

En el siguiente ejemplo completo se detalla cómo se pueden especificar las columnas de la tabla de colección con la anotación @Column:

@Embeddable public class Direccion {
    protected String calle;
    protected String ciudad;
    protected String provincia;
    // ...
}

@Entity public class Persona {
    @Id protected String numeroSeguridadSocial;
    protected String nombre;
    protected Direccion casa;
    //   ...
    @ElementCollection  // usa la tabla por defecto: Persona_Alias
    @Column(name="nombre", length=50)
    protected Set<String> alias = new HashSet();
    //   ...
}

@Entity public class Medico extends Persona {
    @ElementCollection
    @CollectionTable(name="Casa") // usa el nombre por defecto de la clave foránea.
    @AttributeOverrides({
            @AttributeOverride(name="calle", column=@Column(name="calleCasa")),
            @AttributeOverride(name="ciudad", column=@Column(name="ciudadCasa")),
            @AttributeOverride(name="provincia", column=@Column(name="provinciaCasa"))
    })
    protected Set<Direccion> casas = new HashSet();
      //  ...
}

En el ejemplo anterior hemos sobrescrito los nombres de las columnas de la tabla de colección Casa con la anotación @AttributeOverride y @AttributeOverrides.

3. Ordenación de colecciones

3.1. @OrderBy

La anotación @OrderBy se utiliza para ordenar los elementos de una colección. Se puede aplicar a colecciones de elementos básicos o embebidos, así como cualquier relación multivaluada (@OneToMany, @ManyToMany) de tipo List.

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection
        @OrderBy("fechaInicio DESC")
        private List<Vacaciones> vacaciones;
        // ...
    }

En este caso, la colección vacaciones se ordenará por la fecha de inicio de las vacaciones en orden descendente.

El elemento de la anotación @OrderBy es una lista de nombres de propiedades separados por comas, que se utilizan para ordenar los elementos de la colección.

La sintaxis es la siguiente:

    @OrderBy("propiedad1 [ASC|DESC], propiedad2 [ASC|DESC], ...")

Si no se especifica ASC o DESC, el orden por defecto es ascendente (ASC).

orderby_list::= orderby_item [,orderby_item]*
orderby_item::= [property_or_field_name] [ASC | DESC]

Si no se especifica @OrderBy en una asociación de una entidad con una colección de elementos, el orden por defecto es el orden de la clave primaria.

Para referir a una propiedad de un elemento embebido, se puede utilizar la notación de punto. Por ejemplo:

    @Entity 
    public class Persona {
        @Id
        private long idPersona;
        private String nome;
        @ElementCollection
        @OrderBy("rua ASC")
        private List<Direccion> direccions;
        // ...
    }

En este caso, la colección direccions se ordenará por la rua de las direcciones en orden ascendente.

Si se quiere ordenar por una propiedad de un elemento embebido, se puede utilizar la notación de punto. Por ejemplo:

@Entity
public class Persona {

    @ElementCollection
    @OrderBy("codigoPostal.codigoProvincia, codigoPostal.codigoConcello")
    public Set<Direccion> getResidencias() {
        // ...
    }
    //...
}

@Embeddable
public class Direccion {
    protected String rua;
    protected String cidade;
    protected String provincia;
    @Embedded protected CodigoPostal codigoPostal;
}

@Embeddable
public class CodigoPostal {
    protected String codigoProvincia;
    protected String codigoConcello;
}

Para ordenar por varias propiedades, se pueden especificar varias propiedades separadas por comas. Por ejemplo:

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection
        @OrderBy("fechaInicio DESC, duracion ASC")
        private List<Vacaciones> vacaciones;
        // ...
    }

La ordenación puede realizarse para cualquier relación multivaluada de tipo List:

@Entity
public class Curso {
    // ...
    @ManyToMany
    @OrderBy("apelidos ASC")
    public List<Estudante> getEstudantes() {
        //...
    }
    // ...
}

@Entity
public class Estudante {
    // ...
    @ManyToMany(mappedBy="estudantes")
    @OrderBy // ordena por clave primaria
    public List<Curso> getCurso() {
        //...
    }
    // ...
}

3.2. @OrderColumn

Otro tipo de ordenación es por medio de la anotación @OrderColumn que se utiliza para ordenar los elementos de una colección. Se puede aplicar a colecciones de elementos básicos o embebidos, así como cualquier relación multivaluada (@OneToMany, @ManyToMany) de tipo List.

El uso de @OrderColumn es incompatible con @OrderBy (uno u otro, no ambos).

    @Entity 
    public class Estudiante {
        @Id
        private long idEstudiante;
        private String nombre;
        @OneToMany(mappedBy="estudiante")
        @OrderColumn(name="nombre")
        private List<Materia> materias;
        // ...
    }

Si no se especifica el nombre de la columna, se utilizará el nombre de la propiedad de la colección seguido de “_ORDER”.

    @Entity 
    public class Estudiante {
        @Id
        private long idEstudiante;
        private String nombre;
        @OneToMany(mappedBy="estudiante")
        @OrderColumn
        private List<Materia> materias;
        // ...
    }

4. Generación de claves primarias para colecciones de elementos: @CollectionId (Hibernate) (*)

A modo de curiosidad, se puede mencionar que existe una anotación @CollectionId que se utiliza para generar claves primarias para las colecciones de elementos en Hibernate (no en JPA, por lo que este apartado no es relevante para el examen).

La anotación @CollectionId es exclusiva de Hibernate se utiliza para generar claves primarias para las colecciones de elementos. Se puede aplicar a colecciones de elementos básicos o embebidos.

    @Entity 
    public class Persona {
        @Id
        private long id;
        private String nombre;
        @ElementCollection
        @CollectionId(columns=@Column(name="idVacaciones"), type=@Type(type="long"), generator="sequence")
        private Collection vacaciones;
        // ...
    }

En este caso, la colección vacaciones tendrá una clave primaria generada por una secuencia.

5. Ejemplo de mapeo de colecciones

En el siguiente ejemplo se muestra cómo mapear una colección de elementos básicos y una colección de elementos embebidos:

    @Entity 
    public class Persona {
        @Id
        private long idPersona;
        private String nombre;
        @ElementCollection
        @CollectionTable(name="Vacaciones", joinColumns=@JoinColumn(name="idPersona"))
        @Column(name="fechaInicio")
        private Collection<Date> vacaciones;
        @ElementCollection
        @CollectionTable(name="Direccion", joinColumns=@JoinColumn(name="idPersona"))
        @AttributeOverrides({
            @AttributeOverride(name="calle", column=@Column(name="calleCasa")),
            @AttributeOverride(name="ciudad", column=@Column(name="ciudadCasa")),
            @AttributeOverride(name="provincia", column=@Column(name="provinciaCasa"))
        })
        private Collection<Direccion> direcciones;
        // ...
    }

En este caso, la entidad Persona tiene una colección de fechas de vacaciones y una colección de direcciones. La tabla Vacaciones almacenará las fechas de vacaciones y la tabla Direccion almacenará las direcciones de la entidad Persona.

    @Entity 
    public class Curso {
       // ...
       @ManyToMany
       @OrderBy("apelidos ASC")
       public List<Estudiante> getEstudiantes() {
       //    ...
       };
       // ...
    }

En este caso, la colección estudiantes se ordenará por el apellido de los estudiantes en orden ascendente.

@Entity
public class Estudiante {
    //    ...
    @ManyToMany(mappedBy="estudiantes")
    @OrderBy // ordena por clave primaria
    public List<Curso> getCurso() {
        //...
        // 
        }
    //   ...
}

6. One-to-many vs @ElementCollection

Anotaciones One to Many

Necesitamos usar anotaciones One to Many si creamos una relación entre dos entidades (TABLAS).

Podemos representar Tienda, la tienda tiene muchas sucursales, y ambas son una entidad, lo que significa que ambas tienen una tabla dentro de nuestra base de datos.

Tienda.java

@Entity
@Table(name = "tienda")
public class Tienda {
    @Id    
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    private Long idTienda;    
    
    @Column(name = "nome")    
    private String nome; 
    
    @Column(name = "url")    
    private String url;
    
    @OneToMany(mappedBy = "tienda" , cascade = CascadeType.ALL)       
    private Set<Sucursal> sucursales = new HashSet<>();
}

Y Sucursal.java

@Entity
@Table(name = "sucursal")
public class Sucursal {
    @Id    
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    private Long idSucursal;
    
    @Column(name = "nome")    
    private String nome;
    
    @Column(name = "url")    
    private String url;
    
    @ManyToOne
    private Tienda tienda;
}

Después de ejecutar el código, el proveedor de persistencia (Hibernate,..) creará dos tablas dentro de tu base de datos, la primera llamada tienda y la segunda llamada sucursal.

Ahora necesitamos crear un repositorio o una clase DAO para ambas entidades para insertar y obtener los datos de la base de datos.

Si usamos String Boot, podemos usar @Repository para crear un componente Repositorio (lo veremos más adelante). Podemos usar @Repository para crear un componente Repositorio.

TiendaRepository.java

@Repository
public interface TiendaRepository extends JpaRepository<Tienda,Long> {
}

Y ahora necesitamos crear un Repositorio para la entidad Sucursal.

SucursalRepository.java

@Repository
public interface SucursalRepository extends JpaRepository<Sucursal,Long> {
}

Y después de eso, tenemos una nueva relación entre Tienda y Sucursal.

Anotación ElementCollection

@ElementCollection es una anotación JPA estándar (anteriormente con Hibernate se empleaba la anotación propietaria CollectionOfElements).

Significa que la colección no es una colección de entidades, sino una colección de tipos simples (cadenas, etc.) o una colección de elementos integrables/embebidos (clase anotada con @Embeddable).

También significa que los elementos son propiedad completamente de las entidades que los contienen: se modifican cuando se modifica la entidad, se eliminan cuando se elimina la entidad, etc. No pueden tener su propio ciclo de vida.

Ahora veremos cómo podemos implementar con colección de elementos.

La anotación más simple, @ElementCollection, le dice al compilador que estamos asignando una colección, en la que @CollectionTable proporciona el nombre de la tabla objetivo, y luego @JoinColumn especifica la columna real que unimos como se muestra a continuación:

Del último ejemplo, ahora tenemos dos entidades Tienda y Sucursal, y la relación entre ellas, y añadiremos una nueva clase llamada Producto.

Producto.java:

@Embeddable
public class Producto {
    // no necesitamos usar id porque no es una entidad
    @Column(name="nome")
    private String nome;
    
    @Column(name="precio")
    private Double precio;
}

Y ahora añadimos una nueva anotación Elementcollection en la clase Tienda y un objeto producto de la clase Producto.

Tienda.java

@Entity
@Table(name = "tienda")
public class Tienda {
    @Id    
    @GeneratedValue(strategy = GenerationType.IDENTITY)    
    private Long idTienda;
    
    @Column(name = "nome")    
    private String nome;
    
    @Column(name = "url")    
    private String url;
    
    @OneToMany(mappedBy = "tienda" , cascade = CascadeType.ALL)       
    private Set<Sucursal> sucursales = new HashSet<>();
    
    @ElementCollection(fetch = FetchType.LAZY)
    @CollectionTable(name = "ProductoTienda", joinColumns = @JoinColumn(name = "idTienda", nullable = false), uniqueConstraints = @UniqueConstraint(columnNames = {"idTienda"}))
    private Set<Product> products = new HashSet<>();
}

Y ahora después de crearlo la tabla de Producto tiene idTienda como clave primaria. Pero es una clave foránea de la tabla de tienda y no una clave primaria para la tabla de productos.

El objeto Embeddable no tiene un Repositorio/DAO porque depende de la entidad, por ejemplo, el producto embebido depende de la entidad de la Tienda. Y podemos insertarlo sin usar el Repositorio de la Tienda.

La diferencia entre ambos enfoques:

Entidad:

Cuando creamos una entidad, podemos relacionarla con el repositorio/DAO JPA para escribir nuestra consulta o usar la consulta integrada de JPA.

Embeddable:

Cuando creamos una clase integrable, debe relacionarse con cualquier clase de entidad, porque depende de la clase de entidad. y no podemos crear un repositorio.

Resumen

  • @ElementCollection, por otro lado, es muy similar a @OneToMany excepto que el objeto objetivo no es una entidad.
  • Con @ElementCollection, no podemos consultar, persistir o fusionar (merge) objetos objetivo de forma independiente de su objeto principal.
  • No admite operaciones de cascada. Esto significa que los objetos objetivo siempre se persisten, se fusionan o se eliminan junto con su objeto principal.
  • @ElementCollection es una forma fácil de definir una colección con objetos simples/básicos. @OneToMany es el mejor para casos de uso complejos en los que se requiere un control detallado.
Última actualización: 23.09.2025

09. Consultas.

1. Introducción a consultas JPA

Especificación de JPA para consultas

Las consultas JPA son consultas que se realizan sobre las entidades de la base de datos. Estas consultas se pueden realizar de varias formas:

a. Lenguajes de consulta:

  • Consultas JPQL: (Jakarta Persistence Query Language): lenguaje de consulta independiente de bases de datos, orientado a objetos que operqa sobre el modelo de entidades lógico (o sobre el modelo físico de la base de datos). Las consultas JPQL se realizan sobre las entidades de la base de datos y no sobre las tablas de la base de datos. Ejemplo:
    TypedQuery<Empleado> q = em.createQuery("SELECT e FROM Empleado e WHERE e.nome = :nome", Empleado.class);
    q.setParameter("nome", "Otto");
    List<Empleado> resultado = q.getResultList();
  • Consultas nativas SQL: consultas que se realizan sobre la base de datos, utilizando el lenguaje de consulta de la base de datos (SQL). Ejemplo:
    Query q = em.createNativeQuery("SELECT * FROM EMPLEADO WHERE NOME = ?1", Empleado.class);
    q.setParameter(1, "Otto");
    List<Empleado> resultado = q.getResultList();

b. API Criteria: API que se utiliza para construir consultas de forma programática. Esta API se usa para construir consultas basadas en objetos Java y no en cadenas/String de consulta. Ejemplo:

    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Empleado> q = cb.createQuery(Empleado.class);
    Root<Empleado> c = q.from(Empleado.class);
    q.select(c)
        .where(cb.equal(c.get("nome"), "Otto"));
    List<Empleado> resultado = em.createQuery(q).getResultList();

1.1. Métodos de EntityManager para crear consultas

Los métodos de EntityManager que se utilizan para realizar consultas son:

Método Descripción
Query createQuery(String qlString) Crea una instancia de Query para ejecutar una consulta JPQL.
<T> TypedQuery<T> createQuery(String qlString, Class<T> claseResultado) Crea una instancia de TypedQuery de JPQL. La lista de elementos del SELECT debe tener un único elemento, que debe poder asignarse al tipo pasado como argumento, claseResultado.
Query createQuery (CriteriaUpdate updateQuery) Crea una instancia de Query para ejecutar una consulta de un Criteria de actualización.
Query createQuery(CriteriaDelete deleteQuery) Crea una instancia de Query para ejecutar una consulta de eliminación de criterios.
<T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery) Crea una instancia de TypedQuery para ejecutar una consulta de criterios.
Query createNativeQuery(String sqlString) Crea una instancia de Query para ejecutar una declaración SQL nativa, por ejemplo, para actualizar o eliminar. Si la consulta no es de borrado/actualización, devuelve un array de objetos (Object[]).
Query createNativeQuery(String sqlString, Class claseResultado) Crea una instancia de Query para ejecutar una consulta SQL nativa, indicando el tipo de datos revuelto.
Query createNativeQuery(String sqlString, String resultSetMapping) Crea una instancia de Query para ejecutar una consulta SQL nativa. Recoge el nombre del mapeo del conjunto de resultados.

También se pueden crear consultas con nombre, NamedQuery, que se definen en la entidad con la anotación @NamedQuery:

@Entity
@NamedQuery(name="Empleado.findByNome", query="SELECT e FROM Empleado e WHERE e.nome = :nome")
public class Empleado {
    // ...
}

1.2. Métodos de Query

Los principales de la interfaz Query que se utilizan para ejecutar consultas son:

Método Descripción
int executeUpdate() Ejecuta la consulta de actualización o eliminación y devuelve el número de entidades afectadas.
List getResultList() Ejecuta la consulta y devuelve una lista de resultados. Las lista de resultados no es tipada.
default Stream getResultStream() Ejecuta la consulta y devuelve un java.util.stream.Stream no tipado de resultados. El Stream de resultados no es tipada. Por defecto delega al método getResultList().stream(), sin embargo el proveedor de persistencia, Hibernate…, puede sobrescribirlo para mejorar rendimiento y funcionalidades.
Object getSingleResult() Ejecuta la consulta y devuelve el resultado único. Si la consulta devuelve más de un resultado, se lanza una excepción de tipo NonUniqueResultException.
Query setFirstResult(int startPosition) Asigna la posición del primer resultado a recuperar. Útil para paginación.
Query setMaxResults(int maxResult) Establece el número máximo de resultados a recuperar. Útil para paginación.
int getFirstResult() Ejecuta la consulta y devuelve la posición del primer resultado. Devuelve 0 si no se ha aplicado el método setFirstResult. Útil para paginación.
Query setParameter(int position, Object value) Asigna un valor a un parámetro de la consulta.
Query setParameter(String name, Object value) Asigna un valor a un parámetro con nombre de la consulta.
Query setParameter(int position, Date value, TemporalType temporalType) Asigna un valor a un parámetro de tipo temporal (java.util.Date) a la consulta.
Consultas con tipo y excepciones.
  • La interfaz jakarta.persistence.TypedQuery<X> sobrescribe los métodos List<X> getResultList(), default Stream<X> getResultStream() y X getSingleResult() para devolver una lista de elementos de tipo X, un Stream de tipo X y un elemento de tipo X, respectivamente.
  • El método getSingleResult() lanza una excepción de tipo NoResultException si no se encuentran resultados.

Ejemplo completo de consulta JPA

import java.util.Scanner;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Persistence;

import java.util.List;

public class JPAQuery {

    public static Scanner SCAN = new Scanner(System.in);

    public static void main(String[] args) {

        EntityManager em;
        if (args.length != 1) {
            // em = JPAUtil.getEntityManager();
            em = Persistence.createEntityManagerFactory("bibliotecaH2").createEntityManager();
        } else {
//            em = JPAUtil.getEntityManager(args[0]);
            em = Persistence.createEntityManagerFactory(args[0]).createEntityManager();

        }

        System.out.println("Escribe la orden \"salir;\" para salir.");
        boolean salir = false;

        while (!salir) {

            System.out.print("Jakarta Persistence QL> ");
            StringBuilder sb = new StringBuilder();
            do {
                sb.append(" ").append(SCAN.nextLine().trim());
            } while (!sb.toString().endsWith(";"));

            String consulta = sb.substring(0, sb.length() - 1);
            if (!consulta.equalsIgnoreCase("salir")) { //
                if (consulta.isEmpty()) {
                    continue;
                }
                try {
                    if ("select".equalsIgnoreCase(consulta.trim().substring(0, 6))) {
                        // Consulta JPQL. La interfaz TypedQuery hereda de Query
                        // y permite la ejecución de consultas JPQL con la devolución de
                        // resultados tipados.
                        // TypedQuery<?> q = em.createQuery(consulta, Object.class);
                        List<?> resultado = em.createQuery(consulta).getResultList(); // Las wildcard permiten devolver cualquier tipo de objeto
                        if (!resultado.isEmpty()) {
                            int count = 0;
                            for (Object o : resultado) {
                                System.out.print(++count + " ");
                                mostrarResultados(o);
                            }
                        } else {
                            System.out.println("0 resultados de la consulta");
                        }
                    } else {
                        int i = em.createQuery(consulta).executeUpdate();
                        System.out.println(i + " elementos modificados");
                    }
                } catch (Exception e) {
                    System.out.println("Error al procesar  la consulta: " + e.getMessage());
                }
            } else {
                salir = true;
            }
        }
    }

    private static void mostrarResultados(Object resultado) {
        if (resultado == null) {
            System.out.print("NULL");
        } else if (resultado instanceof Object[] fila) {
            System.out.print("[");
            for (Object o : fila) {
                mostrarResultados(o);
            }
            System.out.print("]");
        } else if (resultado instanceof Long || resultado instanceof Double || resultado instanceof String) {
            System.out.print(resultado.getClass().getName() + ": " + resultado);
        } else {
            // ReflectionToStringBuilder es una clase de Apache Commons Lang que
            // permite la conversión de objetos a cadenas de texto.
//            System.out.print(ReflectionToStringBuilder.toString(resultado, ToStringStyle.SHORT_PREFIX_STYLE));
            System.out.print(resultado);
        }
        System.out.println();
    }
}
Bases de datos películas
Modelo de base de datos películas

Ejercicio 09.01. Ejecución de consultas JPA Películas

Ejercicio. Consultas.

Implementa el ejemplo anterior contra una base de datos de películas proporcionada.

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.

Realiza las siguientes consultas:

Las películas que no tienen año de inicio definido:

SELECT p.castelan, p.anoFin, p.anoInicio
FROM Pelicula p
WHERE p.anoInicio IS NOT NULL;

Las películas con una duración superior a 120 minutos:

SELECT p.castelan, p.anoFin, p.duracion
FROM Pelicula p
WHERE p.duracion > 120;

Las películas con Antonio Banderas:

SELECT p FROM Pelicula p JOIN p.personaxes pp JOIN pp.personaxe per WHERE per.nomeOrdenado LIKE 'Antonio Banderas';

Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

2. Jakarta Persistence Query Language (JPQL)

2.1. Historia de JPQL

EL origen de JPQL es Enterprise JavaBeans Query Language (EJB QL), que se introdujo en la especificación EJB 2.0 para permitir a los desarrolladores escribir métodos portables de búsqueda y selección para beans de entidad gestionados por contenedores. Se basaba en un pequeño subconjunto de SQL e introdujo una forma de navegar a través de las relaciones de entidad tanto para seleccionar datos como para filtrar los resultados. Sin embargo, imponía limitaciones estrictas en la estructura de la consulta, limitando los resultados a una única entidad o a un campo persistente de una entidad. Aunque eran posibles las uniones internas entre entidades, se utilizaba una notación extraña. La versión inicial ni siquiera admitía la ordenación.

La especificación EJB 2.1 ajustó EJB QL, añadiendo soporte para la ordenación e introduciendo funciones agregadas básicas; pero nuevamente, la limitación de un único tipo de resultado obstaculizaba el uso de agregados. Se podía filtrar los datos, pero no había equivalente a las expresiones GROUP BY y HAVING de SQL.

Jakarta Persistence QL extiende significativamente EJB QL, eliminando muchas debilidades de las versiones anteriores mientras preserva la compatibilidad hacia atrás. Algunas de las características disponibles añadidas:

  • Tipos de resultados de valor único y múltiple: las consultas JPQL pueden devolver una única entidad, un campo persistente de una entidad, una lista de entidades o una lista de campos persistentes.
  • Funciones agregadas, con cláusulas de ordenación y agrupación: GROUP BY y HAVING.
  • Una sintaxis de unión más natural, incluyendo soporte para inner joins y outer joins: LEFT JOIN y RIGHT JOIN.
  • Expresiones condicionales que involucran subconsultas: EXISTS, ALL, ANY y SOME.
  • Consultas de actualización y eliminación para cambios masivos de datos: UPDATE y DELETE.
  • Proyección de resultados en clases no persistentes: SELECT NEW (ideal para DTOs, Data Transfer Objects).

2.2. Sintaxis de JPQL

La sintaxis de JPQL es similar a la de SQL, pero opera sobre entidades y sus atributos en lugar de tablas y columnas.

Las consultas JPQL se definen como cadenas de texto y se pueden incrustar en el código Java.

Las consultas JPQL se pueden ejecutar de forma dinámica en tiempo de ejecución, lo que permite a las aplicaciones adaptar las consultas a las condiciones cambiantes.

2.2.1. Consultas SELECT

Las consultas SELECT de JPQL se utilizan para recuperar datos de la base de datos. La sintaxis básica de una consulta SELECT es:

    SELECT [DISTINCT] select_expression
    FROM identification_variable_declaration
    [WHERE conditional_expression]
    [GROUP BY grouping_expression]
    [HAVING conditional_expression]
    [ORDER BY ordering_expression [ASC | DESC]]

Una declaración SELECT es una cadena que consta de las siguientes cláusulas:

  • Una cláusula SELECT, que determina el tipo de objetos o valores que se seleccionarán.
  • Una cláusula FROM, que proporciona declaraciones que designan el dominio al cual se aplican las expresiones especificadas en las otras cláusulas de la consulta.
  • Una cláusula WHERE opcional, que se puede utilizar para restringir los resultados que devuelve la consulta.
  • Una cláusula GROUP BY opcional, que permite agregar los resultados de la consulta en términos de grupos.
  • Una cláusula HAVING opcional, que permite filtrar sobre grupos agregados.
  • Una cláusula ORDER BY opcional, que se puede utilizar para ordenar los resultados que devuelve la consulta.

En la sintaxis de BNF, una declaración SELECT se define como:

select_statement ::= select_clause from_clause [where_clause] [groupby_clause] [having_clause] [orderby_clause]

Una declaración SELECT siempre debe tener una cláusula SELECT y una cláusula FROM. Los corchetes cuadrados [] indican que las otras cláusulas son opcionales.

La consulta más simple en Jakarta Persistence QL selecciona todas las instancias de un solo tipo de entidad:

SELECT e
FROM Empleado e

Jakarta Persistence QL utiliza la sintaxis de SQL.

La principal diferencia entre SQL y Jakarta Persistence QL para esta consulta es que, en lugar de seleccionar de una tabla, se ha especificado una entidad del modelo de dominio de la aplicación. La cláusula SELECT de la consulta también es ligeramente diferente, enumerando solo el alias de Empleado e. Esto indica que el tipo de resultado de la consulta es la entidad Empleado, por lo que ejecutar esta instrucción dará como resultado una lista de cero o más instancias de Empleado.

Con un alias se puede navegar a través de las relaciones de entidad utilizando el operador punto (.):

SELECT e.nombre
FROM Empleado e

El campo persistente (e.nombre, en este caso) de la entidad puede ser de tipo simple o incrustable, o a una asociación que conduce a otra entidad o colección de entidades. Dado que la entidad Empleado tiene un campo persistente llamado nombre de tipo String, esta consulta dará como resultado una lista de cero o más objetos String.

También se puede seleccionar una entidad que ni siquiera mencionamos en la cláusula FROM. Consideremos el siguiente ejemplo:

SELECT e.departamento
FROM Empleado e

Un Empleado tiene una relación de muchos a uno con su Departamento llamada departamento, por lo que el tipo de resultado de la consulta es la entidad Departamento.

2.2.2. Filtrado de resultados

Para filtrar resultados se utiliza la cláusula WHERE. La mayoría de operaciones disponibles en SQL están disponibles en JPQL, como:

  • Operadores básicos de comparación: =, >, <, >=, <=, <>.
  • Expresiones: BETWEEN, LIKE, IN, IS NULL, IS NOT NULL.
  • Funciones de cadena: CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE.
  • Funciones aritméticas: ABS, CEILING, EXP, FLOOR, LN, MOD, POWER, ROUND, SIGN, SQRT, SIZE, INDEX.
  • Funciones de fecha: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, EXTRACT, …
  • Funciones de agregado: AVG, COUNT, MAX, MIN, SUM.

Por ejemplo:

SELECT e
FROM Empleado e
WHERE e.salario > 1000

En este caso, la cláusula WHERE se utiliza para filtrar los resultados de la consulta. La condición e.salario > 1000 se evalúa para cada instancia de Empleado en la base de datos, y solo las instancias que satisfacen la condición se incluyen en el resultado de la consulta.

SELECT e
FROM Empleado e
WHERE e.departamento.nombre = 'Ventas' 
  AND e.direccion.ciudad = 'Santiago'

2.2.3. Proyección de resultados

En aquellos casos en los que se encuentre interesado en recuperar solo algunos campos de una entidad, se puede utilizar la cláusula SELECT para proyectar los resultados. Por ejemplo:

SELECT e.nombre, e.salario
FROM Empleado e
WHERE e.salario > 1000

Dependiendo de cómo una entidad está mapeada en la base de datos, puede ser más eficiente recuperar solo algunos campos de una entidad que recuperar la entidad completa. En este caso, la consulta devuelve una lista de objetos Object[], donde cada objeto es un array de dos elementos que contiene el nombre y el salario de un empleado.

Referencias de constructores NEW

Las referencias de constructores son una buena opción para casos de uso de ==solo lectura=. Son más cómodos de usar que las proyecciones de valores escalares y evitan los gastos generales de las entidades administradas.

JPQL le permite definir una llamada al constructor en la cláusula SELECT. Sólo se necesita proporcionar el nombre de clase completo y especificar los parámetros del constructor de un constructor existente. De manera similar a la proyección de entidad, genera una consulta SQL que devuelve las columnas de la base de datos requeridas y utiliza la referencia del constructor para crear una instancia de un nuevo objeto para cada registro en el conjunto de resultados.

SELECT new com.javhoz.ad.jpa.AutorDTO(a.idAutor, a.nome, a.apelidos) FROM Autor a
Resultados de consulta distintos

El operador DISTINCT de SQL que elimina duplicados de una proyección también lo admite JPQL:

SELECT DISTINCT e.departamento
FROM Empleado e
Expresiones condicionales con CASE

Las expresiones condicionales se pueden utilizar en la cláusula WHERE para filtrar los resultados de la consulta. Por ejemplo:

SELECT e
FROM Empleado e
WHERE e.salario > 1000 AND e.salario < 2000

Sin embargo, JPQL admite expresiones CASE: case general, case simple y case de búsqueda.

SELECT e.nombre, 
       CASE WHEN e.salario > 2000 THEN 'Alto' ELSE 'Bajo' END
FROM Empleado e

O también para valores concretos:

SELECT e.nombre, 
       CASE e.salario
           WHEN 1000 THEN 'Bajo'
           WHEN 2000 THEN 'Medio'
           ELSE 'Alto'
       END
FROM Empleado e

Ejemplos:

UPDATE Empleado e
SET e.salario =
    CASE WHEN e.clasificacion = 1 THEN e.salario * 1.1
         WHEN e.clasificacion = 2 THEN e.salario * 1.05
         ELSE e.salario * 1.01
    END

UPDATE Empleado e
SET e.salario =
    CASE e.clasificacion WHEN 1 THEN e.salario * 1.1
                  WHEN 2 THEN e.salario * 1.05
                  ELSE e.salario * 1.01
    END

SELECT e.nombre,
    CASE TYPE(e) WHEN Desarrollador THEN 'Desarrollador'
                 WHEN Administrador THEN 'Administrador'
                 WHEN Profesor THEN 'Profesor'
                 ELSE 'Empleado'
    END
FROM Empleado e
WHERE e.departamento.nombre = 'Sistemas'

SELECT e.nombre,
       f.nombre,
       CONCAT(CASE WHEN f.kmAnuales > 50000 THEN 'Platinum '
                   WHEN f.kmAnuales > 25000 THEN 'Dorada '
                   WHEN f.kmAnuales > 10000 THEN 'Plateada '
                   ELSE ''
              END,
       ' Frecuencia')
FROM Empleado e JOIN e.planDeViaje f

2.2.4. Joins entre entidades

El tipo de resultado de una consulta SELECT no puede ser una colección; debe ser un objeto de valor único, como una instancia de entidad o un tipo de campo persistente.

Expresiones como e.telefonos son ilegales en la cláusula SELECT porque darían como resultado instancias de Collection (cada ocurrencia de e.telefonos es una colección, no una instancia). Por lo tanto, al igual que con SQL y tablas, si queremos navegar a lo largo de una asociación de colección y devolver elementos de esa colección, debemos hacer un join las dos entidades.

Por ejemplo:

SELECT p.numero
FROM Empleado e, Telefono t
WHERE e = t.empleado AND
      e.departamento.nombre = 'Desarrollo' AND
      t.tipo = 'Móvil'

También se pueden expresar en la cláusula FROM utilizando el operador JOIN. La ventaja de este operador es que el join se puede expresar en términos de la propia asociación, y el motor de consulta suministrará automáticamente los criterios de unión necesarios cuando genere el SQL. La consulta anterior se puede reescribir para usar el operador JOIN. Recuerda que el alias p es de tipo Telefono:

SELECT p.numero
FROM Empleado e JOIN e.telefonos p
WHERE e.departamento.nombre = 'Desarrollo' AND
    p.tipo = 'Móvil'

Jakarta Persistence QL admite varios tipos de uniones, incluidas uniones internas y externas, así como una técnica llamada fetch joins para cargar de manera proactiva datos asociados con el tipo de resultado de una consulta pero que no se devuelven directamente.

Inner Joins (Joins relacionados)

Un Inner Join devuelve solo las filas que tienen correspondencias en ambas tablas. En Jakarta Persistence QL, un join interno se expresa con la palabra clave [INNER] JOIN:

[INNER] JOIN join_association_path_expression [AS] identification_variable [join_condition]

Por ejemplo:

SELECT a, p FROM Autor a JOIN a.libros p

La definición de la entidad Autor proporciona toda la información que se necesita para unirla a la entidad Libro, y no es necesario proporcionar una declaración ON adicional.

La palabra INNER es opcional:

SELECT c FROM Cliente c INNER JOIN c.pedidos p WHERE c.estado = 1

Es equivalente a la consulta con el constructor IN:

SELECT OBJECT(c) FROM Cliente c, IN(c.pedidos) p WHERE c.estado = 1

La siguiente consulta se une a Empleado, Información de contacto y Teléfono. ContactoInfo es una clase embedded que consta de una dirección y un conjunto de teléfonos. El teléfono es una entidad.

SELECT t.compañia
FROM Empleado e JOIN e.contactoInfo c JOIN c.telefonos t
WHERE c.direccion.codigoPostal = '15705'

Se puede especificar una condición de unión para una unión interna. Esto equivale a la especificación de la misma condición en la cláusula WHERE.

Left Outer Joins

LEFT JOIN y LEFT OUTER JOIN son sinónimos.

A veces sólo quieres unirte a las entidades relacionadas que cumplen condiciones adicionales.

Este Join perimte recibir un conjunto de entidades cuyos valores asociados a la condición de unión pueden ser nulos.

Sintaxis:

LEFT [OUTER] JOIN join_association_path_expression [AS] identification_variable [join_condition]

Si no se especifica la condición de unión, el join se realiza en función de la relación de asociación.

SELECT s.nombre, COUNT(p)
FROM Proveedor s LEFT JOIN s.productos p
GROUP BY s.nombre

Equivalente a SQL:

SELECT s.nombre, COUNT(p.id)
FROM Proveedor s LEFT JOIN Procucto p
                           ON s.idProveedor = p.idProducto
GROUP By s.nombre

Puede añadirse otra condición a la unión explícita:

SELECT s.nombre, COUNT(p)
FROM Proveedor s LEFT JOIN s.productos p
                           ON p.estado = 'stock'
GROUP BY s.nombre

Equivalente a SQL:

SELECT s.nombre, COUNT(p.id)
FROM Proveedor s LEFT JOIN Producto p
                           ON s.idProveedor = p.idProducto
                           AND p.estado = 'stock'
GROUP By s.nombre
Fetch Joins

Ejemplo de fetch join

Un FETCH JOIN permite la obtención de una asociación o colección de elementos como un efecto secundario de la ejecución de una consulta.

La sintaxis para un fetch join es

fetch_join ::= [LEFT [OUTER] | INNER] JOIN FETCH join_association_path_expression

La asociación referenciada por el lado derecho de la cláusula FETCH JOIN debe ser una asociación o colección de elementos que se referencia desde una entidad o integrable que se devuelve como resultado de la consulta.

No se permite especificar una variable de identificación para los objetos referenciados por el lado derecho de la cláusula FETCH JOIN y, por lo tanto, las referencias a las entidades o elementos obtenidos de manera implícita no pueden aparecer en ninguna otra parte de la consulta.

La siguiente consulta devuelve un conjunto de departamentos. Como efecto secundario, también se recuperan los empleados asociados a esos departamentos, aunque no formen parte del resultado explícito de la consulta. La inicialización del estado persistente o de los campos o propiedades de relación de los objetos que se recuperan como resultado de un fetch join está determinada por los metadatos de esa clase, en este ejemplo, la clase de entidad Empleado.

SELECT d
FROM Departamento d LEFT JOIN FETCH d.empleados
WHERE d.numeroDepartamento = 1

Un fetch join tiene las mismas semánticas de unión que la unión interna u externa correspondiente, excepto que los objetos relacionados especificados en el lado derecho de la operación de unión no se devuelven en el resultado de la consulta ni se hacen referencia de ninguna otra manera en la consulta. Por lo tanto, por ejemplo, si el departamento 1 tiene cinco empleados, la consulta anterior devuelve cinco referencias a la entidad del departamento 1.

2.2.5. Consultas Agregadas

La sintaxis para consultas agregadas en Jakarta Persistence QL es muy similar a la de SQL. Hay cinco funciones agregadas admitidas:

  • AVG.
  • COUNT.
  • MIN.
  • MAX.
  • SUM.

Los resultados pueden agruparse en la cláusula GROUP BY y filtrarse mediante la cláusula HAVING. Nuevamente, la diferencia está en el uso de expresiones de entidad al especificar los datos que se van a agregar. Por ejemplo:

SELECT d, COUNT(e), MAX(e.salario), AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUP BY d
HAVING COUNT(e) >= 5

2.2.6. Parámetros en las consultas

Jakarta Persistence QL admite dos tipos de sintaxis para la vinculación de parámetros:

  • Vinculación posicional, donde los parámetros se indican en la cadena de consulta mediante un signo de interrogación seguido del número de parámetro (similar a JDBC):
SELECT e
FROM Empleado e
WHERE e.departamento = ?1 AND
e.salario > ?2
  • Parámetros con nombre, que se indican en la cadena de consulta mediante dos puntos seguidos del nombre del parámetro:
SELECT e
FROM Empleado e
WHERE e.departamento = :dept AND
e.salario > :base

Ejercicio 09.02. Creación de consultas JPA Películas

a) Obtener todas las películas que tienen una duración mayor a 120 minutos.

b) Obtener todas las películas que pertenecen a un género específico (por ejemplo, “Drama”).

c) Obtener todas las ocupaciones que tienen más de 5 películas asociadas.

d) Obtener todas las películas que tienen un país específico (por ejemplo, “España”).

e) Obtener todas las películas que tienen al menos un personaje interpretado por un actor de un país específico (por ejemplo, “Francia”).

f) Obtener todas las películas que tienen música compuesta por un compositor específico (por ejemplo, “John Williams”).

g) Obtener todas las películas que tienen un personaje interpretado por un actor con un nombre específico (por ejemplo, “Tom Hanks”).

h) Obtener todas las películas que tienen un género específico y que fueron producidas en un año específico (por ejemplo, “Acción” y 2005).

i) Obtener todas las películas que tienen un personaje interpretado por un actor de un género específico (por ejemplo, “Mujer”).

f) Obtener todas las películas que tienen un personaje interpretado por un actor que nació en un país específico y que tienen una duración mayor a 100 minutos.

g) Devolver todos los países que no tienen películas asociadas, puedes usar una consulta JPQL que utilice una subconsulta o un LEFT JOIN con una condición IS NULL.

Ejercicio 09.03. Consultas SQL a JPQL (películas)

Ejercicio. Consultas sobre la base de datos de películas

Amplía el ejercicio anterior la base de datos de películas proporcionada.

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.

Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

Realiza las siguientes consultas:

  1. Muestra la película solicitando el id:
SELECT castelan, orixinal, anoFin, poster IS NOT NULL as tenPoster
FROM pelicula WHERE idPelicula = :identificador
  1. Muestra las películas que tienen algún personaje (IS EMPTY) o no tienen personajes (IS NOT EMPTY).

  2. Muestra las películas que tienen personajes con una ocupación concreta:

SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe AND
PP.ocupacion='OCUPACIÓNCONCRETA' AND PP.idPelicula=IDENTIFICADOR_PELICULA
  1. Muestra los títulos de las películas en las que ha trabajado un actor concreto.

  2. Listar el número de películas de acuerdo con el nombre propocionado: (Crea una clase PeliculaDTO con los campos idPelicula, castelan, orixinal, anoFin, tenPoster (booleano) y realiza la consulta)

SELECT idPelicula, castelan, orixinal, anoFin, poster IS NOT NULL as tenPoster
FROM pelicula WHERE castelan LIKE %:nombre% ORDER BY 5 DESC, castelan ASC
  1. Consulta los datos de las ocupaciones de los personajes de una película:
SELECT O.ocupacion FROM ocupacion O WHERE EXISTS (
SELECT idPelicula FROM peliculapersonaxe PP WHERE 
O.ocupacion=PP.ocupacion 
AND PP.idPelicula=IDENTIFICADOR_DE_PELICULA)
AND  O.orde<>0 ORDER BY O.orde

y los nombres sde los personajes que tienen esa ocupación:

SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe AND
PP.ocupacion='OCUPACIÓNCONCRETA' AND PP.idPelicula=IDENTIFICADOR_PELICULA

3. Definición de Consultas

Jakarta Persistence proporciona las interfaces Query y TypedQuery para configurar y ejecutar consultas.

  • La interfaz Query se utiliza en casos en los que el tipo de resultado es Object o en consultas dinámicas cuando el tipo de resultado puede no ser conocido de antemano.
  • La interfaz TypedQuery es la preferida y se puede utilizar siempre que se conozca el tipo de resultado.

TypedQuery hereda de Query, por lo que una consulta fuertemente tipada siempre se puede tratar como una versión no tipada, aunque no al revés.
Una implementación de la interfaz adecuada para una consulta dada se obtiene a través de uno de los métodos Factory ("createQuery, createNamedQuery, createNativeQuery, createStoredProcedureQuery, getCriteriaBuilder().createQuery") en la interfaz EntityManager (pueden verse en la tabla anterior). La elección del método Factory depende del tipo de consulta (Jakarta Persistence QL, SQL u objeto de criterios), si la consulta ha sido predefinida y si se desean resultados fuertemente tipados (en la tabla anterior pueden verse ejemplos).

Hay tres enfoques para definir una consulta Jakarta Persistence QL:

  • Una consulta puede ser creada dinámicamente en tiempo de ejecución:
    Las consultas dinámicas de Jakarta Persistence QL más que cadenas y, por lo tanto, pueden definirse sobre la marcha según sea necesario.
   Query q = em.createQuery("SELECT e FROM Empleado e WHERE e.departamento = :dept AND e.salario > :base");
   q.setParameter("dept", "Desarrollo");
   q.setParameter("base", 1000);
   List<Empleado> resultado = q.getResultList()
  • Configurada en los metadatos de la unidad de persistencia (por medio de una anotación o XML) y posteriormente ser referenciada por nombre: Las consultas con nombre son estáticas e inalterables, pero son más eficientes de ejecutar porque el proveedor de persistencia puede traducir la cadena de Jakarta Persistence QL a SQL una vez cuando la aplicación comienza, en lugar de cada vez que se ejecuta la consulta.
@NamedQuery(name="Empleado.findByDeptAndSalario", query="SELECT e FROM Empleado e WHERE e.departamento = :dept AND e.salario > :base")
    TypedQuery<Empleado> q = em.createNamedQuery("Empleado.findByDeptAndSalario", Empleado.class);
    q.setParameter("dept", "Desarrollo");
    q.setParameter("base", 1000);
    List<Empleado> resultado = q.getResultList();
  • Especificada dinámicamente y guardada para ser referenciada más adelante por nombre. Definir dinámicamente una consulta y luego darle un nombre permite que una consulta dinámica se reutilice varias veces a lo largo de la vida de la aplicación, pero incurre en el costo de procesamiento dinámico solo una vez.

3.1. Definición Dinámica de Consultas

Una consulta puede definirse dinámicamente pasando la cadena de consulta Jakarta Persistence QL y el tipo de resultado esperado al método createQuery() de la interfaz EntityManager (el tipo de resultado puede omitirse para crear una consulta no tipada).

Se admiten todos los tipos de consultas Jakarta Persistence QL y uso de parámetros. La capacidad de construir una cadena en tiempo de ejecución y usarla para definir una consulta es útil, especialmente para aplicaciones donde el usuario puede especificar criterios complejos y la forma exacta de la consulta no puede conocerse de antemano.

Jakarta Persistence también admite una API Criteria para crear consultas dinámicas mediante objetos Java.

Un problema a considerar con las consultas de cadena dinámicas es el costo de traducir la cadena de Jakarta Persistence QL a SQL para su ejecución. Un motor de consulta típico tendrá que analizar la cadena de Jakarta Persistence QL en un árbol de sintaxis, obtener los metadatos de asignación objeto-relacional para cada entidad en cada expresión y luego generar el SQL equivalente. Para aplicaciones que emiten muchas consultas, el costo de rendimiento del procesamiento dinámico de consultas puede convertirse en un problema.

Muchos motores de consultas almacenarán en caché el SQL traducido para su uso posterior, pero esto se puede vencer fácilmente si la aplicación no utiliza la vinculación de parámetros y concatena los valores de parámetros directamente en las cadenas de consulta. Esto tiene el efecto de generar una consulta nueva y única cada vez que se construye una consulta que requiere parámetros.

Ejemplo, que busca información salarial dado el nombre de un departamento y el nombre de un empleado, nada recomendable:

public class ServicioConsulta {
    @PersistenceContext(unitName="ConsultasDinamicas") /* Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE). */
    EntityManager em;

    public long consultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
        String consulta = "SELECT e.salario " +
            "FROM Empleado e " +
            "WHERE e.departamento.nombre = '" + nombreDepartamento +
            "' AND " +
            " e.nombre = '" + nombreEmpleado + "'";
        return em.createQuery(consulta, Long.class).getSingleResult();
    }
}

. Hay dos problemas: uno relacionado con el rendimiento y otro relacionado con la seguridad:

  • Como los nombres se concatenan en la cadena (en lugar de usar la vinculación de parámetros), efectivamente se crea una consulta nueva y única cada vez. Cien llamadas a este método podrían generar potencialmente cien cadenas de consulta diferentes.
  • En este ejemplo es que es vulnerable a ataques de inyección SQL, donde un usuario malintencionado podría pasar un valor que altera la consulta (por ejemplo, el gerente del departamento está consultando los salarios de sus empleados). Si el nombre fuera el texto “CALQUERA’ OR ‘Pepe’ = ‘Pepe”, la consulta real analizada por el motor de consultas sería la siguiente:
SELECT e.salario
FROM Empleado e
WHERE e.departamento.nombre = 'Desarrollo' AND
      e.nombre = 'CALQUERA' OR
    'Pepe' = 'Pepe'

Al introducir la condición OR, el usuario se ha dado acceso efectivo al valor salarial de cualquier empleado, porque la condición AND original tiene una precedencia más alta que OR.

El uso de parámetros con nombre reduce la cantidad de consultas únicas analizadas por el motor de consultas y elimina la posibilidad de inyección SQL:

public class ServicioConsulta {
    private static final String CONSULTA =
        "SELECT e.salario " +
        "FROM Empleado e " +
        "WHERE e.departamento.nombre = :nombreDepartamento AND " +
        " e.nombre = :nombreEmpleado ";

    @PersistenceContext(unitName="UnidadConsultas") // Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE).
    EntityManager em; // Cuando se inyecta el EntityManager, se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas".

    public long consultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
        return em.createQuery(CONSULTA, Long.class)
            .setParameter("nombreDepartamento", nombreDepartamento)
            .setParameter("nombreEmpleado", nombreEmpleado)
            .getSingleResult();
    }
}

Los parámetros se empaquetan utilizando la API de JDBC y son manejados directamente por la base de datos. El texto de una cadena de parámetros se cita efectivamente por la base de datos, por lo que el ataque malicioso realmente produciría la siguiente consulta:

SELECT e.salario
FROM Empleado e
WHERE e.departamento.nombre = 'Desarrollo' AND
      e.nombre = 'CALQUERA'' OR
    ''Pepe'' = ''Pepe'

Las comillas simples se escapan añadiéndoles una comilla simple adicional. Esto elimina cualquier significado especial de ellas y toda la secuencia se trata como un único valor de cadena.

Recomendación

Se recomienda el uso de consultas con nombre definidas estáticamente, especialmente para consultas que se ejecutan con frecuencia (siguiente sección). Si las consultas dinámicas son necesarias debe usarse la vinculación de parámetros en lugar de concatenar valores de parámetros en cadenas de consulta para minimizar la cantidad de cadenas de consulta distintas analizadas por el motor de consultas.

3.2. Consultas con nombre

Las consultas nombradas permiten organizar las definiciones de consultas y mejorar el rendimiento de la aplicación.

Una consulta nombrada se define utilizando la anotación @NamedQuery, que puede colocarse en la definición de clase para cualquier entidad. La anotación define el nombre de la consulta, así como el texto de la consulta:

Declaración de una consulta nombrada:

@NamedQuery(name="encontrarSalarioPorNombreYDepartamento",
query="SELECT e.salario FROM Empleado e " +
"WHERE e.departamento.nombre = :nombreDepartamento AND " +
" e.nombre = :nombreEmpleado")

Las consultas nombradas se colocan normalmente en la clase de entidad que más corresponde directamente al resultado de la consulta, por lo que la entidad Empleado sería un buen lugar para esta consulta nombrada.

La anotación @NamedQuery puede aparecer varias veces en una clase de entidad. Esto es útil si la entidad tiene varias consultas que se utilizan con frecuencia. Las consultas nombradas se pueden referenciar por nombre en cualquier lugar donde se pueda usar una consulta dinámica. Tienen dos elementos obligatorios:

  • name: String con el nombre de la consulta.
  • query: String con el texto de la consulta.

Uso de una consulta nombrada:

public class ServicioConsulta {
    @PersistenceContext(unitName="UnidadConsultas") // Se usa cuando se inyecta el EntityManager de la unidad de persistencia "UnidadConsultas" y es gestionado por el contenedor (no para aplicaciones Java SE).
    EntityManager em; 

    public long consultarSalarioEmpleado(String nombreDepartamento, String nombreEmpleado) {
        return em.createNamedQuery("encontrarSalarioPorNombreYDepartamento", Long.class)
            .setParameter("nombreDepartamento", nombreDepartamento)
            .setParameter("nombreEmpleado", nombreEmpleado)
            .getSingleResult();
    }
}

https://thorben-janssen.com/jpql/

Última actualización: 23.09.2025

10. JPQL.

1. Lenguaje de consulta de Jakarta Persistence (JPQL)

Especificación de JPQL

La API de Persistencia de Jakarta proporciona dos métodos para consultar entidades:

  • El lenguaje de consulta de la API de Persistencia de Jakarta (Jakarta Persistence QL).
  • API Criteria.

1.1. Jakarta Persistence QL

Jakarta Persistence QL es el lenguaje de consulta estándar basado en String de la API de Persistencia de Jakarta.

Es un lenguaje de consulta portable que combina la sintaxis y la semántica de SQL con la de un lenguaje de expresión orientado a objetos. Las consultas escritas utilizando este lenguaje pueden compilarse de forma portátil a SQL en todos los servidores de bases de datos principales.

1.2. API de Criterios

La API Criteria se utiliza para crear consultas seguras para el tipo utilizando las API del lenguaje de programación Java al consultar entidades y sus relaciones.

De momento, no estudiaremos el API de Criteria, y nos centraremos en JPQL.

2. Introducción a Jakarta Persistence QL

  • Jakarta Persistence QL no es SQL. A pesar de las similitudes entre los dos lenguajes en cuanto a palabras clave y estructura general, existen diferencias muy importantes. Intentar escribir Jakarta Persistence QL como si fuera SQL es la forma más fácil de frustrarse con el lenguaje. Las similitudes entre ambos lenguajes son intencionales (brindando a los desarrolladores una idea de lo que Jakarta Persistence QL puede lograr), pero la naturaleza orientada a objetos de Jakarta Persistence QL requiere un tipo diferente de pensamiento.

Naturaleza de Jakarta Persistence QL

Si Jakarta Persistence QL no es SQL, Jakarta Persistence QL es un lenguaje para consultar entidades.

En lugar de tablas y filas, proporciona consultas en términos de entidades y sus relaciones, operando sobre el estado persistente de la entidad definido en el modelo de objetos, no en el modelo físico de la base de datos.

  1. A diferencia de SQL, es portable. Jakarta Persistence QL se puede traducir a los dialectos SQL de todos los principales proveedores de bases de datos.
  2. Las consultas se escriben contra el modelo de dominio de entidades persistentes, sin necesidad de saber exactamente cómo se asignan esas entidades a la base de datos.

APIs de Jakarta Persistence QL vs Criteria:

  • Las consultas de Jakarta Persistence QL son generalmente más concisas y legibles que las consultas de Criteria. Jakarta Persistence QL es fácil de aprender para programadores con conocimientos previos de SQL.

  • Las consultas de Jakarta Persistence QL no son seguras en cuanto a tipos, lo que significa que requieren una conversión cuando se recupera el resultado de la consulta del administrador de entidades. Debido a eso, los errores de conversión de tipo pueden no detectarse en tiempo de compilación. Además, las consultas de Jakarta Persistence QL no admiten parámetros de final abierto.

  • Las consultas de Criteria API son seguras en cuanto a tipos y, por lo tanto, no requieren conversión.

  • En cuanto a rendimiento entre Jakarta Persistence QL y Criteria APIs, las consultas de Criteria API proporcionan un mejor rendimiento porque las consultas dinámicas de Jakarta Persistence QL deben analizarse cada vez que se llaman.

Una de las desventajas comunes de las consultas de Criteria API es que suelen requerir escribir más código que las consultas de Jakarta Persistence QL. Esto significa que los programadores deberán crear muchos objetos y realizar operaciones en esos objetos antes de enviar la consulta de Criteria API al administrador de entidades.

Una amplia selección de características de SQL son compatibles directamente, incluidas subconsultas, consultas de agregados, declaraciones UPDATE y DELETE, numerosas funciones de SQL, y más.

2.1. Terminología

Las consultas se dividen en una de cuatro categorías: select, aggregate, update y delete.

  • Las consultas select recuperan el estado persistente de una o más entidades, filtrando los resultados según sea necesario
  • Las consultas de agregación son variaciones de las consultas select que agrupan los resultados y producen datos resumidos. Juntas, las consultas select y de agregado a veces se llaman consultas de informes, ya que se centran principalmente en generar datos para informes.
  • Las consultas de actualización y eliminación se utilizan para modificar o eliminar condicionalmente conjuntos enteros de entidades.

Las consultas operan en el conjunto de entidades y embebidos definidos por una unidad de persistencia. Este conjunto de entidades y embebidos se conoce como el esquema de persistencia abstracto, cuya colección define el dominio general desde el cual se pueden recuperar los resultados.

En las expresiones de consulta, las entidades se refieren por nombre. Si una entidad no ha sido nombrada explícitamente (por ejemplo, utilizando el atributo de nombre de la anotación @Entity), se utiliza el nombre de clase no calificado de forma predeterminada. Este nombre es el nombre de esquema abstracto de la entidad en el contexto de una consulta.

Las entidades están compuestas por una o más propiedades de persistencia implementadas como campos o propiedades JavaBean. El tipo de esquema abstracto de una propiedad persistente en una entidad se refiere a la clase o tipo primitivo utilizado para implementar esa propiedad. Por ejemplo, si la entidad Empleado tiene una propiedad nombre de tipo String, el tipo de esquema abstracto de esa propiedad en las expresiones de consulta también es String. Las propiedades persistentes simples sin asignación de relaciones comprenden el estado persistente de la entidad y se denominan campos de estado. Las propiedades persistentes que también son relaciones se llaman campos de asociación.

Las consultas se pueden definir dinámicamente o estáticamente. Los ejemplos incluyen consultas que pueden usarse dinámicamente o estáticamente, según las necesidades de la aplicación.

Finalmente, es importante destacar que las consultas no distinguen entre mayúsculas y minúsculas excepto en dos casos: los nombres de entidades y propiedades deben especificarse exactamente como se nombran.

3. Consultas SELECT

Las consultas SELECT son el tipo de consulta más significativo y facilitan la recuperación masiva de datos de la base de datos. No sorprendentemente, las consultas SELECT también son la forma más común de consulta utilizada en las aplicaciones. La forma general de una consulta SELECT es la siguiente:

SELECT <select_expression>
FROM <from_clause>
[WHERE <conditional_expression>]
[ORDER BY <order_by_clause>]

La forma más simple de una consulta SELECT consta de dos partes obligatorias: la cláusula SELECT y la cláusula FROM. La cláusula SELECT define el formato de los resultados de la consulta, mientras que la cláusula FROM define la entidad o entidades de las cuales se obtendrán los resultados. Considere la siguiente consulta completa que recupera todos los empleados en la empresa:

SELECT e
FROM Empleado e

La estructura de esta consulta es muy similar a una consulta SQL, pero con un par de diferencias importantes. La primera diferencia es que el dominio de la consulta definido en la cláusula FROM no es una tabla, sino una entidad, en este caso, la entidad Empleado. Como en SQL, se le ha asignado un alias al identificador e. Este valor alias se conoce como una variable de identificación y es la clave por la cual la entidad se referirá en el resto de la declaración SELECT. A diferencia de las consultas en SQL, donde un alias de tabla es opcional, el uso de variables de identificación es obligatorio en Jakarta Persistence QL.

La segunda diferencia es que la cláusula SELECT en este ejemplo no enumera los campos de la tabla ni utiliza un comodín para seleccionar todos los campos. En cambio, solo se enumera la variable de identificación para indicar que el tipo de resultado de la consulta es la entidad Empleado, no un conjunto tabular de filas.

Procesamiento de Consultas y Resultados

A medida que el procesador de consultas itera sobre el conjunto de resultados devuelto por la base de datos, convierte los datos de filas y columnas tabulares en un conjunto de instancias de entidad. El método getResultList() de la interfaz Query devolverá una colección de cero o más objetos Empleado después de evaluar la consulta. A pesar de las diferencias en estructura y sintaxis, cada consulta es traducible a SQL.

Para ejecutar una consulta, el motor de consultas primero construye una representación SQL óptima de la consulta de Jakarta Persistence QL. La consulta SQL resultante es la que realmente se ejecuta en la base de datos. En este ejemplo simple, el SQL podría parecer algo así, dependiendo de los metadatos de mapeo para la entidad Empleado:

SELECT id, nombre, salario, idGestor, idDepartamento, idDireccion
FROM emp

La instrucción SQL debe leer todas las columnas mapeadas necesarias para crear la instancia de entidad, incluidas las columnas de clave externa. Incluso si la entidad está en caché en la memoria, el motor de consultas aún leerá típicamente todos los datos necesarios para asegurarse de que la versión en caché esté actualizada. Tenga en cuenta que si las relaciones entre Empleado y las entidades Departamento o Direccion requirieran una carga ansiosa (eager loading), la instrucción SQL se extendería para recuperar los datos adicionales o se agruparían múltiples instrucciones para construir completamente la entidad Empleado. Cada proveedor proporcionará algún método para mostrar el SQL que genera al traducir Jakarta Persistence QL. Para la optimización del rendimiento en particular, comprender cómo se aborda la generación de SQL por parte de su proveedor puede ayudarlo a escribir consultas más eficientes.

Ahora que hemos revisado una consulta simple y cubierto la terminología básica, las siguientes secciones pasarán por cada una de las cláusulas de la consulta SELECT, explicando la sintaxis y las características disponibles.

Nota: la función de transmisión de consultas incluida en Jakarta Persistence nos ayudará a evitar la recuperación de demasiados datos y provocar errores. Sin embargo, aún se recomienda y es más eficiente utilizar el método de paginación de Result Set.

3.1. Cláusula SELECT

La cláusula SELECT de una consulta puede tener varias formas, incluyendo expresiones de ruta simples y complejas, expresiones escalares, expresiones de constructor, funciones agregadas y secuencias de estos tipos de expresiones. Las siguientes secciones introducen las expresiones de ruta y discuten los diferentes estilos de cláusulas SELECT y cómo determinan el tipo de resultado de la consulta. Dejamos la discusión de las expresiones escalares para explorar las expresiones condicionales en la cláusula WHERE. Están completamente descritas en la sección llamada “Expresiones Escalares”. Las funciones agregadas se detallan en el apartado “Consultas Agregadas”.

3.1.1. Expresiones de Ruta

Se utilizan para navegar desde una entidad, ya sea a través de una relación hacia otra entidad (o colección de entidades) o hacia una de las propiedades persistentes de una entidad. La navegación que resulta en uno de los campos de estado persistentes (ya sea campo o propiedad) de una entidad se denomina ruta de campo de estado. La navegación que lleva a una sola entidad se llama ruta de asociación de un solo valor, mientras que la navegación hacia una colección de entidades se llama ruta de asociación de un valor de colección.

El operador de punto (.) significa navegación de ruta en una expresión. Por ejemplo, si la entidad Empleado se ha asignado a la variable de identificación e, e.nombre es una expresión de ruta de campo de estado que resuelve al nombre del empleado. De manera similar, la expresión de ruta e.departamento es una asociación de un solo valor desde el empleado hacia el departamento al que está asignado. Finalmente, e.directos es una asociación de un valor de colección que se resuelve en la colección de empleados que reportan a un empleado que también es un gerente.

Lo que hace que las expresiones de ruta sean tan poderosas es que no están limitadas a una sola navegación. En su lugar, las expresiones de navegación se pueden encadenar para recorrer grafos de entidades complejos siempre que la ruta se mueva de izquierda a derecha a través de asociaciones de un solo valor. Una ruta no puede continuar desde un campo de estado o una asociación de un valor de colección.

Usando esta técnica, podemos construir expresiones de ruta como e.departamento.nombre, que es el nombre del departamento al que pertenece el empleado. Tenga en cuenta que las expresiones de ruta pueden navegar hacia objetos incrustados y a través de ellos, así como a entidades normales. La única restricción en los objetos incrustados en una expresión de ruta es que la raíz de la expresión de ruta debe comenzar con una entidad.

Las expresiones de ruta se utilizan en cada cláusula de una consulta SELECT, determinando desde el tipo de resultado de la consulta hasta las condiciones bajo las cuales se deben filtrar los resultados. La experiencia con las expresiones de ruta es clave para escribir consultas efectivas.

3.1.2. Entidades y Objetos

La primera y más simple forma de la cláusula SELECT es una única variable de identificación. El tipo de resultado para una consulta de este estilo es la entidad a la que está asociada la variable de identificación. Por ejemplo, la siguiente consulta devuelve todos los departamentos en la empresa:

SELECT d
FROM Departamento d

La palabra clave OBJECT se puede utilizar para indicar que el tipo de resultado de la consulta es la entidad vinculada a la variable de identificación. No tiene impacto en la consulta, pero se puede usar como una pista visual:

SELECT OBJECT(d)
FROM Departamento d

El único problema de usar OBJECT es que, aunque las expresiones de ruta pueden resolverse a un tipo de entidad, la sintaxis de la palabra clave OBJECT está limitada a variables de identificación. La expresión OBJECT(e.departamento) es ilegal incluso si Departamento es un tipo de entidad. Por esa razón, no se recomienda la sintaxis de OBJECT. Existe principalmente por compatibilidad con versiones anteriores del lenguaje que requerían la palabra clave OBJECT bajo el supuesto de que una revisión futura de SQL incluiría la misma terminología.

Una expresión de ruta que resuelve a un campo de estado o asociación de un solo valor también se puede utilizar en la cláusula SELECT. En este caso, el tipo de resultado de la consulta se convierte en el tipo de la expresión de ruta, ya sea el tipo de campo de estado o el tipo de entidad de una asociación de un solo valor. La siguiente consulta devuelve los nombres de todos los empleados:

SELECT e.nombre
FROM Empleado e

El tipo de resultado de la expresión de ruta en la cláusula SELECT es String, por lo que ejecutar esta consulta con getResultList() producirá una colección de cero o más objetos String.

Las expresiones de ruta que resuelven en campos de estado también se pueden usar como parte de expresiones escalares, lo que permite transformar el campo de estado en los resultados de la consulta. Discutiremos esta técnica más adelante en la sección llamada “Expresiones Escalares”.

Las entidades alcanzadas desde una expresión de ruta también se pueden devolver. La siguiente consulta demuestra devolver una entidad diferente como resultado de la navegación de la ruta:

SELECT e.departamento
FROM Empleado e

El tipo de resultado de esta consulta es la entidad Departamento porque es el resultado de atravesar la relación del departamento desde Empleado hasta Departamento. Ejecutar la consulta, por lo tanto, dará como resultado una colección de cero o más objetos Departamento, incluidos duplicados.

Para eliminar los duplicados, se debe utilizar el operador DISTINCT:

SELECT DISTINCT e.departamento
FROM Empleado e

El operador DISTINCT es funcionalmente equivalente al operador del mismo nombre en SQL. Una vez que se recopila el conjunto de resultados, se eliminan los valores duplicados (usando la identidad de la entidad si el tipo de resultado de la consulta es una entidad), de modo que solo se devuelven resultados únicos.

El tipo de resultado de una consulta SELECT es el tipo correspondiente a cada fila en el conjunto de resultados producido al ejecutar la consulta. Esto puede incluir entidades, tipos primitivos y otros tipos de atributos persistentes, pero nunca un tipo de colección. La siguiente consulta es ilegal:

SELECT d.empleados
FROM Departamento d

La expresión de ruta d.empleados es una ruta de valor de colección que produce un tipo de colección. Restringir las consultas de esta manera evita que el proveedor tenga que combinar filas sucesivas de la base de datos en un solo objeto de resultado.

Es posible seleccionar objetos incrustables navegados en una expresión de ruta. La siguiente consulta devuelve solo los objetos incrustables InformacionContacto para todos los empleados:

SELECT e.informacionContacto
FROM Empleado e

Lo importante de recordar al seleccionar objetos incrustables es que los objetos devueltos no estarán gestionados. Si emites una consulta para devolver empleados (SELECT e FROM Empleado e) y luego, desde los resultados, navegas a sus objetos incrustados InformacionContacto, estarías obteniendo objetos incrustables que estaban gestionados. Los cambios en cualquiera de esos objetos se guardarían cuando se confirmara la transacción. Sin embargo, cambiar cualquiera de los resultados de objetos InformacionContacto devueltos de una consulta que seleccionó directamente InformacionContacto, no tendría ningún efecto persistente.

3.1.3 Combinación de Expresiones

Se pueden especificar múltiples expresiones en la misma cláusula SELECT separándolas con comas. El tipo de resultado de la consulta en este caso es un array de tipo Object, donde los elementos del array son los resultados de resolver las expresiones en el orden en que aparecieron en la consulta.

Considera la siguiente consulta que devuelve solo el nombre y salario de un empleado:

SELECT e.nombre, e.salario
FROM Empleado e

Cuando se ejecuta esto, se devolverá una colección de cero o más instancias de arrays de tipo Object. Cada array en este ejemplo tiene dos elementos, el primero es un String que contiene el nombre del empleado y el segundo es un Double que contiene el salario del empleado. La práctica de informar solo un subconjunto de los campos de estado de una entidad se llama proyección porque los datos de la entidad se proyectan desde la entidad en forma tabular. La proyección es una técnica útil para aplicaciones web en las que solo se muestran unos pocos datos de un gran conjunto de instancias de entidades. Dependiendo de cómo se haya mapeado la entidad, podría requerir una consulta SQL compleja para recuperar completamente el estado de la entidad. Si solo se requieren dos campos, el esfuerzo adicional invertido en la construcción de la instancia de entidad podría haber sido desperdiciado. Una consulta de proyección que devuelve solo la cantidad mínima de datos es más útil en estos casos.

3.1.4 Constructor de expresiones

Una forma más potente de la cláusula SELECT que implica múltiples expresiones es la expresión de construcción (NEW), que especifica que los resultados de la consulta se deben almacenar utilizando un tipo de objeto especificado por el usuario. Considera la siguiente consulta:

SELECT NEW ejemplo.DetalleEmpleado(e.nombre, e.salario, e.departamento.nombre)
FROM Empleado e

El tipo de resultado de esta consulta es la clase Java ejemplo.DetalleEmpleado. A medida que el procesador de consultas itera sobre los resultados de la consulta, se crean instancias de DetalleEmpleado utilizando el constructor que coincide con los tipos de expresión enumerados en la consulta. En este caso, los tipos de expresión son String, Double y String. Cada fila en la colección de consulta resultante es una instancia de DetalleEmpleado que contiene el nombre del empleado, el salario y el nombre del departamento.

El tipo de objeto de resultado debe referirse utilizando el nombre completo del objeto. Sin embargo, la clase no tiene que estar mapeada en la base de datos de ninguna manera. Cualquier clase con un constructor compatible con las expresiones enumeradas en la cláusula SELECT se puede usar en una expresión de constructor.

Las expresiones de constructor son herramientas poderosas para construir objetos de transferencia de datos o de vista de grano grueso para su uso en otros niveles de la aplicación. En lugar de construir manualmente estos objetos, se puede usar una sola consulta para reunir objetos de vista listos para su presentación en una página web.

3.2. Cláusula FROM

La cláusula FROM se utiliza para declarar una o más variables de identificación, opcionalmente derivadas de relaciones unidas, que forman el dominio sobre el cual la consulta debería extraer sus resultados. La sintaxis de la cláusula FROM consiste en una o más variables de identificación y declaraciones de cláusulas JOIN.

3.2.1 Variables de Identificación

La variable de identificación es el punto de partida para todas las expresiones de consulta. Cada consulta debe tener al menos una variable de identificación definida en la cláusula FROM, y esa variable debe corresponder a un tipo de entidad. Cuando una declaración de variable de identificación no utiliza una expresión de ruta (es decir, cuando es un solo nombre de entidad), se denomina declaración de variable de rango. Esta terminología proviene de la teoría de conjuntos, ya que se dice que la variable abarca la entidad.

Las declaraciones de variables de rango utilizan la sintaxis <nombre_entidad> [AS] <identificador>.

La palabra clave AS opcional. El identificador debe seguir las reglas de nomenclatura estándar de Java y se puede referenciar en toda la consulta de manera insensible a mayúsculas y minúsculas. Se pueden especificar múltiples declaraciones separándolas con comas.

Las expresiones de ruta también pueden tener un alias con las variables de identificación en el caso de uniones y subconsultas. La sintaxis para las declaraciones de variables de identificación en estos casos se cubrirá en las dos secciones siguientes.

3.2.2. Joins

Una operación de unión (join) es una consulta que combina resultados de varias entidades. Los Joins en Jakarta Persistence QL son lógicamente equivalentes a los de SQL. En última instancia, una vez que la consulta se traduce a SQL, es muy probable que las uniones entre entidades produzcan uniones similares entre las tablas a las que están mapeadas las entidades. Comprender cuándo ocurren las uniones es importante para escribir consultas eficientes.

Las uniones ocurren siempre que se cumplen cualquiera de las siguientes condiciones en una consulta SELECT:

  • Se enumeran dos o más declaraciones de variables de rango en la cláusula FROM y aparecen en la cláusula SELECT.
  • Se utiliza el operador JOIN para extender una variable de identificación mediante una expresión de ruta.
  • Una expresión de ruta en cualquier lugar de la consulta navega a través de un campo de asociación, ya sea a la misma entidad o a una entidad diferente.
  • Una o más condiciones WHERE comparan atributos de diferentes variables de identificación.

La semántica de una unión entre entidades es la misma que las uniones SQL entre tablas. La mayoría de las consultas contienen una serie de condiciones de unión, que son expresiones que definen las reglas para emparejar una entidad con otra.

Las condiciones de unión se pueden especificar explícitamente, como el uso del operador JOIN en la cláusula FROM de una consulta, o implícitamente como resultado de la navegación de la ruta.

Una Inner Join entre dos entidades devuelve los objetos de ambos tipos de entidad que satisfacen todas las condiciones de unión. La navegación de la ruta de una entidad a otra es una forma de unión interna.

La Outer Join de dos entidades es el conjunto de objetos de ambos tipos de entidad que satisfacen las condiciones de unión, más el conjunto de objetos de un tipo de entidad (designado como la entidad izquierda) que no tienen una condición de unión coincidente en el otro.

En ausencia de condiciones de unión entre dos entidades, las consultas producirán un producto cartesiano. Cada objeto del primer tipo de entidad se emparejará con cada objeto del segundo tipo de entidad, multiplicando el número de resultados. Los productos cartesianos son raros en las consultas de Jakarta Persistence QL dadas las capacidades de navegación del lenguaje, pero son posibles si se especifican dos declaraciones de variables de rango en la cláusula FROM sin condiciones adicionales especificadas en la cláusula WHERE.

A. Inner Join

Como lenguaje relacional, Jakarta Persistence QL admite consultas que se basan en múltiples entidades y las relaciones entre ellas.

Las uniones internas entre dos entidades se pueden especificar de dos formas:

  • La primera y preferida forma, porque es explícita y obvia que se está produciendo una unión, es el operador JOIN en la cláusula FROM.
  • Otra forma requiere múltiples declaraciones de variables de rango en la cláusula FROM y condiciones de la cláusula WHERE para proporcionar las condiciones de la unión.
A.1. Operador JOIN y campos de asociación de colección (one-to-many y many-to-many)

La sintaxis de una unión interna mediante el operador JOIN es [INNER] JOIN <expresión_de_ruta> [AS] <identificador>. Considera la siguiente consulta:

SELECT p
FROM Empleado e JOIN e.telefonos p

Esta consulta utiliza el operador JOIN para unir la entidad Empleado con la entidad Telefono a través de la relación telefonos. La condición de unión en esta consulta está definida por el mapeo objeto-relacional de la relación telefonos. No es necesario especificar criterios adicionales para vincular las dos entidades. Al unir las dos entidades, esta consulta devuelve todas las instancias de la entidad Telefono asociadas a los empleados de la empresa.

La sintaxis para las uniones es similar a las expresiones JOIN admitidas por ANSI SQL. Otra forma equivalente en SQL escrita utilizando la forma de join tradicional:

SELECT p.id, p.numeroTelefono, p.tipo, p.idEmpleado
FROM emp e, telefono p
WHERE e.id = p.idEmpleado

El mapeo de la tabla para la entidad Telefono reemplaza la expresión e.telefonos. La cláusula WHERE también incluye los criterios necesarios para unir las dos tablas a través de las columnas de unión definidas por el mapeo de telefonos.

Ten en cuenta que la relación telefonos se ha asignado a la variable de identificación p. Aunque la entidad Telefono no aparece directamente en la consulta, el objetivo de la relación telefonos es la entidad Telefono, y esto determina el tipo de la variable de identificación. Esta determinación implícita del tipo de variable de identificación puede llevar algo de tiempo acostumbrarse. Es necesario estar familiarizado con cómo se definen las relaciones en el modelo de objetos para navegar a través de una consulta escrita.

Cada ocurrencia de p fuera de la cláusula FROM ahora se refiere a un solo teléfono propiedad de un empleado. Aunque se especificó un campo de asociación de colección en la cláusula JOIN, la variable de identificación realmente se refiere a entidades alcanzadas por esa asociación, no a la colección en sí. Ahora se puede usar la variable como si la entidad Telefono estuviera listada directamente en la cláusula FROM. Por ejemplo, en lugar de devolver instancias de la entidad Telefono, se pueden devolver solo los números de teléfono:

SELECT p.numero
FROM Empleado e JOIN e.telefonos p

En la definición de expresiones de ruta anterior, se señaló que una ruta no podía continuar desde un campo de estado o un campo de asociación de colección. Para resolver esta situación, el campo de asociación de colección debe unirse en la cláusula FROM para que se cree una nueva variable de identificación para la ruta, lo que permite que sea la raíz de nuevas expresiones de ruta.

IN VS. JOIN

EJB QL, según lo definido por las especificaciones de EJB, utilizaba un operador especial llamado IN en la cláusula FROM para mapear asociaciones de colecciones a variables de identificación. El soporte para este operador se trasladó a Jakarta Persistence QL. La forma equivalente de la consulta utilizada anteriormente en esta sección podría especificarse como:

SELECT DISTINCT p
FROM Empleado e, IN(e.telefonos) p

El operador IN tiene la intención de indicar que la variable p es una enumeración de la colección telefonos. Sin embargo, el operador JOIN es una forma más potente y expresiva de declarar relaciones y es el operador recomendado para consultas.

A.2. Operador JOIN y campos de asociación de un solo valor (one-to-one y many-to-one)

El operador JOIN funciona tanto con expresiones de ruta de asociación de valor de colección como con expresiones de ruta de asociación de valor único. Considera el siguiente ejemplo:

SELECT d
FROM Empleado e JOIN e.departamento d

Esta consulta define un join desde Empleado hasta Departamento a través de la relación departamento. Esto es semánticamente equivalente a usar una expresión de ruta en la cláusula SELECT para obtener el departamento del empleado. Por ejemplo, la siguiente consulta debería resultar en representaciones SQL similares, si no idénticas, que involucren un join entre las entidades Empleado y Departamento:

SELECT e.departamento
FROM Empleado e

El caso de uso principal para usar una expresión de ruta de asociación de valor único en la cláusula FROM (en lugar de solo usar una expresión de ruta en la cláusula SELECT) es para joins externos. La navegación de la ruta es equivalente al inner join de todas las entidades asociadas atravesadas en la expresión de ruta.

La posibilidad de joins internos implícitos resultantes de expresiones de ruta es algo a tener en cuenta. Considera el siguiente ejemplo que devuelve los departamentos distintos basados en California que participan en el proyecto Versión1:

SELECT DISTINCT e.departamento
FROM Proyecto p JOIN p.empleados e
WHERE p.nombre = 'Versión1' AND
e.direccion.provincia = 'PO'

En realidad, hay cuatro joins lógicos aquí, no dos. El traductor tratará la consulta como si se hubiera escrito con joins explícitos entre las diversas entidades. Cubriremos la sintaxis para múltiples joins más adelante en la sección “Múltiples Joins”, pero por ahora considera la siguiente consulta que es equivalente a la consulta anterior, leyendo las condiciones de join de izquierda a derecha:

SELECT DISTINCT d
FROM Proyecto p JOIN p.empleados e JOIN e.departamento d JOIN e.direccion a
WHERE p.nombre = 'Versión1' AND
a.provincia = 'PO'

Decimos cuatro joins lógicos porque el mapeo físico real podría involucrar más tablas. En este caso, las entidades Empleado y Proyecto están relacionadas a través de una asociación many-to-many que utiliza una tabla de join. Por lo tanto, el SQL real para tal consulta usa cinco tablas, no cuatro. La consulta se vería así:

SELECT DISTINCT d.id, d.nombre
FROM project p, proyectosEmpleado ep, emp e, dept d, direccion a
WHERE p.id = ep.idProyecto AND
ep.idEmpleado = e.id AND
e.idDepartamento = d.id AND
e.idDireccion = a.id AND
p.nombre = 'Versión1' AND
a.provincia = 'PO'

La primera forma de la consulta ciertamente es más fácil de leer y entender. Sin embargo, durante la optimización del rendimiento, podría ser útil comprender cuántos joins pueden ocurrir como resultado de expresiones de ruta aparentemente triviales.

A.3. Condiciones de Join en la Cláusula WHERE

Las consultas SQL tradicionalmente han unido tablas enumerando las tablas a unir en la cláusula FROM y proporcionando criterios en la cláusula WHERE de la consulta para determinar las condiciones de join. Para unir dos entidades sin usar una relación, se utiliza una declaración de variable de rango para cada entidad en la cláusula FROM.

El ejemplo de join anterior entre las entidades Empleado y Departamento también podría haberse escrito de la siguiente manera:

SELECT DISTINCT d
FROM Departamento d, Empleado e
WHERE d = e.departamento

Este estilo de consulta se utiliza generalmente para compensar la falta de una relación explícita entre dos entidades en el modelo de dominio. Por ejemplo, no hay una asociación entre la entidad Departamento y el Empleado que es el gerente del departamento.

Podemos usar una condición de join en la cláusula WHERE para hacer esto posible:

SELECT d, m
FROM Departamento d, Empleado m
WHERE d = m.departamento AND
m.directos IS NOT EMPTY

En este ejemplo, estamos utilizando una de las expresiones de colección especiales, IS NOT EMPTY, para verificar que la colección de informes directos al empleado no está vacía. Cualquier empleado con una colección no vacía de informes directos es, por definición, un gerente.

A.4. Múltiples Joins

Más de un join se puede concatenar si es necesario. Por ejemplo, la siguiente consulta devuelve el conjunto distinto de proyectos pertenecientes a empleados que pertenecen a un departamento:

SELECT DISTINCT p
FROM Departamento d JOIN d.empleados e JOIN e.proyectos p

El procesador de consultas interpreta la cláusula FROM de izquierda a derecha. Una vez que se ha declarado una variable, puede ser referenciada posteriormente por otras expresiones JOIN. En este caso, la relación proyectos de la entidad Empleado se navega una vez que se ha declarado la variable empleado.

A.5. Joins con Mapas: VALUE, KEY y ENTRY

Una expresión de ruta que navega a través de una asociación de valor de colección implementada como un Map es un caso especial. A diferencia de una colección normal, cada elemento en un mapa corresponde a dos piezas de información: la clave y el valor.

Al trabajar con Jakarta Persistence QL, es importante tener en cuenta que, por defecto, las variables de identificación basadas en mapas se refieren al valor. Por ejemplo, considera el caso en el que la relación telefonos de la entidad Empleado se modela como un mapa, donde la clave es el tipo de número (trabajo, celular, casa, etc.) y el valor es el número de teléfono. La siguiente consulta enumera los números de teléfono de todos los empleados:

SELECT e.nombre, p
FROM Empleado e JOIN e.telefonos p

Este comportamiento se puede resaltar explícitamente mediante el uso de la palabra clave VALUE. Por ejemplo, la consulta anterior es funcionalmente idéntica a la siguiente:

SELECT e.nombre, VALUE(p)
FROM Empleado e JOIN e.telefonos p

Para acceder a la clave en lugar del valor para un elemento de mapa dado, podemos usar la palabra clave KEY para anular el comportamiento predeterminado y devolver el valor de la clave para un elemento de mapa dado:

SELECT e.nombre, KEY(p), VALUE(p)
FROM Empleado e JOIN e.telefonos p
WHERE KEY(p) IN ('Trabajo', 'Móvil')

Finalmente, en caso de que queramos que tanto la clave como el valor se devuelvan juntos en forma de un objeto java.util.Map.Entry, podemos especificar la palabra clave ENTRY de la misma manera. Ten en cuenta que la palabra clave ENTRY solo se puede usar en la cláusula SELECT. Las palabras clave KEY y VALUE también se pueden usar como parte de expresiones condicionales en las cláusulas WHERE y HAVING de la consulta.

Es importante señalar que en cada uno de los ejemplos de unión de mapas, unimos una entidad contra uno de sus atributos de Mapa y obtenemos una clave, un valor o un par clave-valor (entrada). Sin embargo, cuando se ve desde la perspectiva de las tablas, la unión se realiza solo a nivel de la clave principal de la entidad de origen y los valores en el Mapa. Actualmente, no hay una facilidad disponible en Jakarta Persistence para unir la entidad de origen contra las claves del Mapa.

B. Outer Joins

Un outer join entre dos entidades produce un dominio en el cual solo se requiere que un lado de la relación esté completo.

En otras palabras, el join externo de Empleado a Departamento a través de la relación de departamento de empleado devuelve todos los empleados y el departamento al cual se le ha asignado el empleado, pero el departamento se devuelve solo si está disponible. Esto contrasta con un inner join que devolvería solo aquellos empleados asignados a un departamento.

Un join externo se especifica utilizando la siguiente sintaxis: LEFT [OUTER] JOIN <path_expression> [AS] <identifier>. La siguiente consulta demuestra un join externo entre dos entidades:

SELECT e, d
FROM Empleado e LEFT JOIN e.departamento d

Si el empleado no ha sido asignado a un departamento, el objeto de departamento (el segundo elemento del array de Object) será nulo.

En una generación SQL típica del proveedor, verás que la consulta anterior sería equivalente a la siguiente:

SELECT e.id, e.nombre, e.salario, e.idGestor, e.idDepartamento, e.idDireccion,
d.id, d.nombre
FROM empleado e LEFT OUTER JOIN departamento d
ON (d.id = e.idDepartamento)

El SQL resultante muestra que cuando se genera un join externo desde Jakarta Persistence QL, siempre especifica una condición ON de igualdad entre la columna de join que mapea la relación que se está uniendo y la clave primaria a la que se está haciendo referencia.

Se puede suministrar una expresión ON adicional para agregar restricciones a los objetos que se devuelven desde el lado derecho del join. Por ejemplo, podemos modificar la consulta Jakarta Persistence QL anterior para tener una condición ON adicional que limite los departamentos devueltos solo a aquellos que tienen un prefijo ‘QA’:

SELECT e, d
FROM Empleado e LEFT JOIN e.departamento d
ON d.nombre LIKE 'De%'

Esta consulta sigue devolviendo todos los empleados, pero los resultados no incluirán ningún departamento que no coincida con la condición ON agregada. El SQL generado se vería así:

SELECT e.id, e.nombre, e.salario, e.idDepartamento, e.idGestor, e.idDireccion,
d.id, d.nombre
FROM empleado e left outer join departamento d
ON ((d.id = e.idDepartamento) and (d.nombre like 'De%'))

Es importante señalar que esta consulta es muy diferente de usar una expresión WHERE:

SELECT e, d
FROM Empleado e LEFT JOIN e.departamento d
WHERE d.nombre LIKE 'De%'

La expresión WHERE limitará los resultados después de realizar el join, lo que puede resultar en un comportamiento diferente:

SELECT e, d
FROM Empleado e LEFT JOIN e.departamento d
WHERE d.nombre LIKE 'De%'

La cláusula WHERE resulta en una semántica de inner join entre Empleado y Departamento, por lo que esta consulta solo devolvería los empleados que estuvieran en un departamento con un nombre que comience con ‘QA’.

C. Fetch Joins

Los fetch joins están destinados a ayudar a los diseñadores de aplicaciones a optimizar el acceso a su base de datos y preparar los resultados de la consulta para su desprendimiento. Permiten que las consultas especifiquen una o más relaciones que deben ser navegadas y pre-cargadas por el motor de consulta para que no se carguen de forma diferida más tarde en tiempo de ejecución.

Por ejemplo, si tenemos una entidad Empleado con una relación de carga diferida con su dirección, la siguiente consulta se puede utilizar para indicar que la relación debe resolverse de forma inmediata durante la ejecución de la consulta:

SELECT e
FROM Empleado e JOIN FETCH e.direccion

No se establece una variable de identificación para la expresión de ruta e.direccion. Esto se debe a que aunque la entidad Direccion se está uniendo para resolver la relación, no es parte del tipo de resultado de la consulta. El resultado de ejecutar la consulta sigue siendo una colección de instancias de la entidad Empleado, excepto que la relación de dirección en cada entidad no causará una consulta secundaria a la base de datos cuando se acceda a ella. Esto también permite que la relación de dirección se acceda de forma segura si la entidad Empleado se vuelve desprendida.

Un fetch join se distingue de un join regular al agregar la palabra FETCH al operador JOIN.

Para implementar fetch joins, el proveedor necesita convertir la asociación recuperada en un join regular del tipo apropiado: interno por defecto o externo si se especificó la palabra LEFT. La expresión SELECT de la consulta también necesita expandirse para incluir la relación unida. Expresado en Jakarta Persistence QL, una interpretación equivalente del proveedor del ejemplo anterior de fetch join se vería así:

SELECT e, a
FROM Empleado e JOIN e.direccion a

La única diferencia es que el proveedor en realidad no devuelve las entidades Direccion al llamante. Debido a que los resultados se procesan desde esta consulta, el motor de consultas crea la entidad Direccion en la memoria y la asigna a la entidad Empleado, pero luego la elimina de la colección de resultados que construye para el cliente. Esto carga de forma anticipada la relación de direcciones, que luego se puede acceder mediante la navegación desde la entidad Empleado.

Una consecuencia de implementar fetch joins de esta manera es que la obtención de una colección asociativa produce resultados duplicados. Por ejemplo, considere una consulta de departamento donde la relación empleados de la entidad Departamento se obtiene de manera anticipada. La consulta de fetch join, esta vez utilizando un outer join para asegurar que se recuperen los departamentos sin empleados, se escribiría de la siguiente manera:

SELECT d
FROM Departamento d LEFT JOIN FETCH d.empleados

Expresado en Jakarta Persistence QL, la interpretación del proveedor reemplazaría el fetch con un outer join a través de la relación empleados:

SELECT d, e
FROM Departamento d LEFT JOIN d.empleados e

Nuevamente, a medida que se procesan los resultados, la entidad Empleado se construye en la memoria pero se elimina de la colección de resultados. Cada entidad Departamento ahora tiene una colección de empleados completamente resuelta, pero el cliente recibe una referencia a cada departamento por empleado. Por ejemplo, si se recuperaron cuatro departamentos con cinco empleados cada uno, el resultado sería una colección de 20 instancias de Departamento, con cada departamento duplicado cinco veces. Las instancias reales de las entidades apuntan todas a las mismas versiones administradas, pero los resultados son algo extraños, como mínimo.

Para eliminar los valores duplicados, se debe usar el operador DISTINCT o los resultados deben colocarse en una estructura de datos como un Set. Dado que no es posible escribir una consulta SQL que use el operador DISTINCT y al mismo tiempo preserve la semántica del fetch join, el proveedor deberá eliminar duplicados en la memoria después de que se hayan recuperado los resultados. Esto podría tener implicaciones de rendimiento para conjuntos de resultados grandes.

Dadas los resultados algo peculiares generados por un fetch join a una colección, puede que no sea la forma más apropiada de cargar de forma anticipada entidades relacionadas en todos los casos. Si una colección requiere una carga anticipada de manera regular, considera hacer que la relación sea ansiosa por defecto. Algunos proveedores de persistencia también ofrecen lecturas por lotes como alternativa a los fetch joins, emitiendo múltiples consultas en un solo lote y luego correlacionando los resultados para cargar de forma anticipada las relaciones. Otra alternativa es utilizar un gráfico de entidades para determinar dinámicamente los atributos de relación que se cargarán mediante una consulta.

3.3. Clausula WHERE

La cláusula WHERE de una consulta se utiliza para especificar condiciones de filtrado para reducir el conjunto de resultados. En esta sección, exploramos las características de la cláusula WHERE y los tipos de expresiones que se pueden formar para filtrar los resultados de la consulta.

La definición de la cláusula WHERE es engañosamente simple. Es simplemente la palabra clave WHERE, seguida de una expresión condicional. Sin embargo, como demuestran las siguientes secciones, Jakarta Persistence QL admite un conjunto poderoso de expresiones condicionales para filtrar las consultas más sofisticadas.

Parámetros de entrada

Los parámetros de entrada para las consultas se pueden especificar utilizando notación posicional o con nombre. La notación posicional se define prefijando el número de variable con un signo de interrogación. Considere la siguiente consulta:

SELECT e
FROM Empleado e
WHERE e.salario > ?1

Utilizando la interfaz Query, cualquier valor double o un valor compatible con el tipo del atributo salario se puede vincular al primer parámetro para indicar el límite inferior para los salarios de los empleados en esta consulta. El mismo parámetro posicional puede ocurrir más de una vez en la consulta. El valor vinculado al parámetro se sustituirá en cada una de sus ocurrencias.

Los parámetros con nombre se especifican utilizando dos puntos seguidos de un identificador. Aquí está la misma consulta, esta vez usando un parámetro con nombre:

SELECT e
FROM Empleado e
WHERE e.salario > :sal

Forma Básica de Expresión

Gran parte del soporte para expresiones condicionales en Jakarta Persistence QL se toma directamente de SQL. Esto es intencional y ayuda a facilitar la transición para los desarrolladores que ya están familiarizados con SQL. La principal diferencia entre las expresiones condicionales en Jakarta Persistence QL y SQL es que las expresiones en Jakarta Persistence QL pueden aprovechar las variables de identificación y las expresiones de ruta para navegar por las relaciones durante la evaluación de la expresión.

Las expresiones condicionales se construyen de la misma manera que las expresiones condicionales en SQL, utilizando una combinación de operadores lógicos, expresiones de comparación, operaciones primitivas y funciones en campos, entre otros. Aunque se proporciona un resumen de los operadores más adelante, la gramática de las expresiones condicionales no se repite aquí. La especificación de Jakarta Persistence contiene la gramática en la forma de Backus-Naur (BNF) y es el lugar donde buscar las reglas exactas sobre el uso de las expresiones básicas. Sin embargo, las secciones siguientes explican los operadores y expresiones de nivel superior, especialmente aquellos únicos de Jakarta Persistence QL, y proporcionan ejemplos para cada uno.

La sintaxis literal también es similar a SQL (consulte la sección “Literales”).

La precedencia de operadores es la siguiente:

  1. Operador de navegación (.)
  2. Unary +/–
  3. Multiplicación (*) y división (/)
  4. Adición (+) y sustracción (–)
  5. Operadores de comparación =, >, >=, <, <=, <>, [NOT] BETWEEN, [NOT] LIKE, [NOT] IN, IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF]
  6. Operadores lógicos (AND, OR, NOT)

Expresiones BETWEEN

El operador BETWEEN se puede usar en expresiones condicionales para determinar si el resultado de una expresión cae dentro de un rango inclusivo de valores. Las expresiones numéricas, de cadena y de fecha se pueden evaluar de esta manera. Considere el siguiente ejemplo:

SELECT e
FROM Empleado e
WHERE e.salario BETWEEN 40000 AND 45000

Cualquier empleado que gane $40,000–$45,000 inclusivamente se incluirá en los resultados. Esto es idéntico a la siguiente consulta que utiliza operadores de comparación básicos:

SELECT e
FROM Empleado e
WHERE e.salario >= 40000 AND e.salario <= 45000

El operador BETWEEN también se puede negar con el operador NOT.

Expresiones LIKE

Jakarta Persistence QL admite la condición LIKE de SQL para proporcionar una forma limitada de coincidencia de patrones de cadena. Cada expresión LIKE consta de una expresión de cadena a buscar y una cadena de patrón y una secuencia de escape opcional que define las condiciones de coincidencia. Los caracteres comodín utilizados por la cadena de patrón son el guion bajo (_) para comodines de un solo carácter y el signo de porcentaje (%) para comodines de varios caracteres:

SELECT d
FROM Departamento d
WHERE d.nombre LIKE '__Eng%'

Estamos utilizando un prefijo de dos guiones bajos para comodinar los primeros dos caracteres de los candidatos de cadena, por lo que los nombres de departamento de ejemplo que coincidirían con esta consulta serían CAEngOtt o USEngCal, pero no CADocOtt. Tenga en cuenta que las coincidencias de patrones distinguen entre mayúsculas y minúsculas.

Si la cadena de patrón contiene un guion bajo o un signo de porcentaje que debe coincidir literalmente, se puede utilizar la cláusula ESCAPE para especificar un carácter que, al anteponerse a un carácter comodín, indica que debe tratarse literalmente:

SELECT d
FROM Departamento d
WHERE d.nombre LIKE 'QA\_%' ESCAPE '\'

Al escapar el guion bajo, se convierte en una parte obligatoria de la expresión. Por ejemplo, QA_East coincidiría, pero QANorth no lo haría.

Subconsultas

Las subconsultas se pueden utilizar en las cláusulas WHERE y HAVING de una consulta. Una subconsulta es una consulta de selección completa dentro de un par de paréntesis que está incrustada dentro de una expresión condicional. Los resultados de ejecutar la subconsulta (que será un resultado escalar o una colección de valores) se evalúan luego en el contexto de la expresión condicional. Las subconsultas son una técnica poderosa para resolver los escenarios de consulta más complejos.

Considere la siguiente consulta:

SELECT e
FROM Empleado e
WHERE e.salario = (SELECT MAX(emp.salario)
FROM Empleado emp)

Esta consulta devuelve el empleado con el salario más alto entre todos los empleados. Se utiliza una subconsulta que consiste en una consulta de agregado (descrita más adelante en este capítulo) para devolver el valor máximo del salario, y luego este resultado se utiliza como clave para filtrar la lista de empleados por salario. Una subconsulta se puede utilizar en la mayoría de las expresiones condicionales y puede aparecer en el lado izquierdo o derecho de una expresión.

El alcance de un nombre de variable de identificación comienza en la consulta donde se define y se extiende hacia abajo en cualquier subconsulta. Las identificadores en la consulta principal pueden ser referenciados por una subconsulta, y los identificadores introducidos por una subconsulta pueden ser referenciados por cualquier subconsulta que cree. Si una subconsulta declara un identificador de variable con el mismo nombre, anula la declaración principal y evita que la subconsulta haga referencia a la variable principal.

Nota: No se garantiza que la anulación de un nombre de variable de identificación en una subconsulta sea compatible con todos los proveedores. Se deben usar nombres únicos para garantizar la portabilidad.

La capacidad de hacer referencia a una variable desde la consulta principal en la subconsulta permite que ambas consultas estén correlacionadas. Considere el siguiente ejemplo:

SELECT e
FROM Empleado e
WHERE EXISTS (SELECT 1
FROM Telefono p
WHERE p.empleado = e AND p.tipo = 'Móvil')

Esta consulta devuelve a todos los empleados que tienen un número de teléfono celular. Esto también es un ejemplo de una subconsulta que devuelve una colección de valores. La expresión EXISTS en este ejemplo devuelve true si la subconsulta devuelve algún resultado. Devolver el literal 1 desde la subconsulta es una práctica estándar con expresiones EXISTS porque los resultados reales seleccionados por la subconsulta no importan; solo es relevante el número de resultados.

Ten en cuenta que la cláusula WHERE de la subconsulta hace referencia a la variable de identificación e de la consulta principal y la utiliza para filtrar los resultados de la subconsulta. Conceptualmente, se puede pensar que la subconsulta se ejecuta una vez por cada empleado. En la práctica, muchos servidores de bases de datos optimizarán este tipo de consultas en uniones o vistas en línea para maximizar el rendimiento.

Esta consulta también podría haberse escrito utilizando una unión entre las entidades Empleado y Telefono con el operador DISTINCT utilizado para filtrar los resultados. La ventaja de usar la subconsulta correlacionada es que la consulta principal permanece libre de uniones con otras entidades. Con frecuencia, si se utiliza una unión solo para filtrar los resultados, existe una condición de subconsulta equivalente que alternativamente se puede utilizar para eliminar restricciones en la cláusula JOIN de la consulta principal o incluso para mejorar el rendimiento de la consulta.

La cláusula FROM de una subconsulta también puede crear nuevas variables de identificación a partir de expresiones de ruta utilizando una variable de identificación de la consulta principal. Por ejemplo, la consulta anterior también podría haberse escrito de la siguiente manera:

SELECT e
FROM Empleado e
WHERE EXISTS (SELECT 1
FROM e.telefonos p
WHERE p.tipo = 'Móvil')

En esta versión de la consulta, la subconsulta utiliza la ruta de la asociación de colecciones telefonos desde la variable de identificación del empleado e en la subconsulta. Luego, esto se asigna a una variable de identificación local p que se utiliza para filtrar los resultados por tipo de teléfono. Cada ocurrencia de p se refiere a un solo teléfono asociado al empleado.

Para ilustrar mejor cómo el traductor maneja esta consulta, considera la consulta equivalente escrita en SQL:

SELECT e.id, e.nombre, e.salario, e.idGestor, e.idDepartamento, e.idDireccion
FROM emp e
WHERE EXISTS (SELECT 1
FROM telefono p
WHERE p.idEmpleado = e.id AND
p.tipo = 'Móvil')

La expresión e.telefonos se convierte en la tabla mapeada por la entidad Telefono. La cláusula WHERE para la subconsulta luego agrega la condición de unión necesaria para correlacionar la subconsulta con la consulta principal, en este caso, la expresión p.idEmpleado = e.id. Los criterios de unión aplicados a la tabla PHONE dan como resultado todos los teléfonos propiedad del empleado relacionado.

Expresiones IN

La expresión IN se puede utilizar para verificar si una expresión de ruta de valor único es un miembro de una colección. La colección se puede definir en línea como un conjunto de valores literales o se puede derivar de una subconsulta. La siguiente consulta demuestra la notación literal al seleccionar a todos los empleados que viven en Nueva York o California:

SELECT e
FROM Empleado e
WHERE e.direccion.provincia IN ('CO', 'PO')

La forma de subconsulta de la expresión es similar, reemplazando la lista literal con una consulta anidada. La siguiente consulta devuelve empleados que trabajan en departamentos que contribuyen a proyectos que comienzan con el prefijo QA:

SELECT e
FROM Empleado e
WHERE e.departamento IN (SELECT DISTINCT d
FROM Departamento d JOIN d.empleados de JOIN
de.proyectos p
WHERE p.nombre LIKE 'De%')

La expresión IN también se puede negar utilizando el operador NOT. Por ejemplo, la siguiente consulta devuelve todas las entidades Telefono que representan números de teléfono que no son para la oficina ni para el hogar:

SELECT p
FROM Telefono p
WHERE p.tipo NOT IN ('Oficina', 'Casa')

Expresiones de Colecciones

El operador IS EMPTY es el equivalente lógico de IS NULL, pero para colecciones. Las consultas pueden usar el operador IS EMPTY o su forma negada IS NOT EMPTY para verificar si una ruta de asociación de colecciones se resuelve a una colección vacía o tiene al menos un valor. Por ejemplo, la siguiente consulta devuelve todos los empleados que son gerentes por tener al menos un informe directo:

SELECT e
FROM Empleado e
WHERE e.directos IS NOT EMPTY

Ten en cuenta que las expresiones IS EMPTY se traducen a SQL como expresiones de subconsultas. El traductor de consultas puede usar una subconsulta de agregado o usar la expresión SQL EXISTS. Por lo tanto, la siguiente consulta es equivalente a la anterior:

SELECT m
FROM Empleado m
WHERE (SELECT COUNT(e)
FROM Empleado e
WHERE e.jefe = m) > 0

El operador MEMBER OF y su forma negada NOT MEMBER OF son una forma abreviada de verificar si una entidad es un miembro de una ruta de asociación de colecciones. La siguiente consulta devuelve todos los gerentes que están incorrectamente registrados como informando a sí mismos:

SELECT e
FROM Empleado e
WHERE e MEMBER OF e.directos

Un uso más típico del operador MEMBER OF es en conjunción con un parámetro de entrada. Por ejemplo, la siguiente consulta selecciona a todos los empleados que están asignados a un proyecto especificado:

SELECT e
FROM Empleado e, Proyecto p
WHERE p = :proyectoSeleccionado AND e MEMBER OF p.empleados

EXISTS

La condición EXISTS devuelve true si una subconsulta devuelve alguna fila. Se mostraron ejemplos de EXISTS anteriormente en la introducción a las subconsultas. El operador EXISTS también se puede negar con el operador NOT. La siguiente consulta selecciona a todos los empleados que no tienen un teléfono celular:

SELECT e
FROM Empleado e
WHERE NOT EXISTS (SELECT p
FROM e.telefonos p
WHERE p.tipo = 'Móvil')

ANY, ALL y SOME

Los operadores ANY, ALL y SOME se pueden utilizar para comparar una expresión con los resultados de una subconsulta. Considera el siguiente ejemplo:

SELECT e
FROM Empleado e
WHERE e.directos IS NOT EMPTY AND
e.salario < ALL (SELECT d.salario
FROM e.directos d)

Esta consulta devuelve los gerentes que ganan menos que todos los empleados que trabajan para ellos. La subconsulta se evalúa y luego se compara cada valor de la subconsulta con la expresión izquierda, en este caso, el salario del gerente. Cuando se usa el operador ALL, la comparación entre el lado izquierdo de la ecuación y todos los resultados de la subconsulta debe ser verdadera para que la condición general sea verdadera.

El operador ANY se comporta de manera similar, pero la condición general es verdadera siempre y cuando al menos una de las comparaciones entre la expresión y el resultado de la subconsulta sea verdadera. Por ejemplo, si se especificara ANY en lugar de ALL en el ejemplo anterior, el resultado de la consulta sería todos los gerentes que ganaban menos que al menos uno de sus empleados. El operador SOME es un alias para el operador ANY.

Hay simetría entre las expresiones IN y el operador ANY. Considera la siguiente variación del ejemplo anterior del departamento de proyectos, modificado ligeramente para usar ANY en lugar de IN:

SELECT e
FROM Empleado e
WHERE e.departamento = ANY (SELECT DISTINCT d
FROM Departamento d JOIN d.empleados de
JOIN de.proyectos p
WHERE p.nombre LIKE 'De%')

3.4. Herencia y Polimorfismo

Jakarta Persistence admite la herencia entre entidades. Como resultado, el lenguaje de consultas admite resultados polimórficos donde se pueden devolver múltiples subclases de una entidad mediante la misma consulta.

En el modelo de ejemplo, Proyecto es una clase base para ProyectoDesarrollo y ProyectoDocumentacion. Si se forma una variable de identificación a partir de la entidad Proyecto, los resultados de la consulta incluirán una mezcla de objetos Proyecto, ProyectoDesarrollo y ProyectoDocumentacion, y los resultados se pueden convertir a las subclases según sea necesario. La siguiente consulta recupera todos los proyectos con al menos un empleado:

SELECT p
FROM Proyecto p
WHERE p.empleados IS NOT EMPTY

3.4.1. Discriminación de Subclases

Si queremos restringir el resultado de la consulta a una subclase particular, podemos utilizar esa subclase específica en la cláusula FROM en lugar de la raíz. Sin embargo, si queremos restringir los resultados a más de una subclase en la consulta pero no a todas las subclases, podemos usar una expresión de tipo en la cláusula WHERE para filtrar los resultados. Una expresión de tipo consiste en la palabra clave TYPE seguida de una expresión entre paréntesis que se resuelve en una entidad. El resultado de una expresión de tipo es el nombre de la entidad, que luego se puede utilizar para comparaciones de tipo. La ventaja de una expresión de tipo es que podemos distinguir entre tipos sin depender de un mecanismo de discriminación en el propio modelo de dominio.

El siguiente ejemplo demuestra el uso de una expresión TYPE para devolver solo proyectos de diseño y calidad:

SELECT p
FROM Proyecto p
WHERE TYPE(p) = ProyectoDocumentacion OR TYPE(p) = ProyectoDesarrollo

Ten en cuenta que no hay comillas alrededor de los identificadores ProyectoDocumentacion y ProyectoDesarrollo. Estos se tratan como nombres de entidad en Jakarta Persistence QL, no como cadenas. A pesar de esta distinción, los parámetros de entrada se pueden usar en lugar de nombres codificados en las cadenas de consulta. Crear una consulta parametrizada que devuelva instancias de un tipo de subclase dado es sencillo, como se ilustra en la siguiente consulta:

SELECT p
FROM Proyecto p
WHERE TYPE(p) = :projectType

3.4.2. Downcasting

En la mayoría de los casos, al menos una de las subclases contiene algún estado adicional, como el atributo clasificacion en ProyectoDesarrollo. Un atributo de subclase se puede acceder directamente si la consulta abarca solo las entidades de la subclase, pero cuando la consulta abarca una superclase, se debe utilizar la conversión descendente (downcasting). La conversión descendente es la técnica de hacer que una expresión que se refiere a una superclase se aplique a una subclase específica. Se logra mediante el uso del operador TREAT.

TREAT se puede usar en la cláusula WHERE para filtrar los resultados según el estado del subtipo de las instancias. La siguiente consulta devuelve todos los proyectos de diseño más todos los proyectos de calidad que tienen una calificación de calidad mayor a 4:

SELECT p
FROM Proyecto p
WHERE TREAT(p AS ProyectoDesarrollo).clasificacion > 4
OR TYPE(p) = ProyectoDocumentacion

La sintaxis de la expresión comienza con la palabra clave TREAT, seguida de su argumento entre paréntesis. El argumento es una expresión de ruta, seguida de la palabra clave AS y luego del nombre de entidad del subtipo objetivo. La expresión de ruta debe resolverse a una superclase del tipo objetivo. La expresión de conversión descendente resultante se resuelve al subtipo objetivo, por lo que se pueden agregar cualquiera de los atributos específicos del subtipo a la expresión de ruta resultante, al igual que se hizo con clasificacion en el ejemplo.

Se pueden incluir varias expresiones TREAT en la cláusula WHERE, cada una haciendo la conversión descendente al mismo o a un tipo de entidad diferente.

Normalmente, cuando se realiza una unión, incluye todas las subclases del tipo de entidad objetivo en la relación que se está uniendo. Para limitar la unión y considerar solo una jerarquía de subclases específica, se puede usar una expresión TREAT en la cláusula FROM. Asignarle un identificador proporciona la ventaja adicional de que el identificador se puede referenciar tanto en la cláusula WHERE como en la cláusula SELECT. La siguiente consulta devuelve todos los empleados que trabajan en proyectos de calidad con una calificación de calidad mayor a 4, más el nombre del proyecto en el que trabajan y su calificación de calidad:

SELECT e, q.nombre, q.clasificacion
FROM Empleado e JOIN TREAT(e.proyectos AS ProyectoDesarrollo) q
WHERE q.clasificacion > 4

La expresión TREAT se puede usar de manera similar para otros tipos de uniones, como uniones externas (outer joins) y uniones de recuperación (fetch joins).

Es importante entender el impacto que tiene la herencia entre entidades en el SQL generado por razones de rendimiento.

3.5. Expresiones escalares

Una expresión escalar es un valor literal, una secuencia aritmética, una expresión de función, una expresión de tipo o una expresión de caso que se resuelve a un solo valor escalar. Se puede utilizar en la cláusula SELECT para dar formato a los campos proyectados en consultas de informes o como parte de expresiones condicionales en la cláusula WHERE o HAVING de una consulta. Las subconsultas que se resuelven a valores escalares también se consideran expresiones escalares, pero solo se pueden usar al componer criterios en la cláusula WHERE de una consulta. Las subconsultas nunca se pueden utilizar en la cláusula SELECT.

3.5.1. Literales

Existen varios tipos de literales que se pueden utilizar en Jakarta Persistence QL, incluyendo cadenas, numéricos, booleanos, enumeraciones, tipos de entidad y tipos temporales.

A lo largo de este capítulo, hemos mostrado muchos ejemplos de literales de cadena, entero y booleano. Las comillas simples se utilizan para delimitar literales de cadena y se escapan dentro de una cadena prefijando la comilla con otra comilla simple. Los numéricos exactos y aproximados se pueden definir según las convenciones del lenguaje de programación Java o utilizando la sintaxis estándar SQL-92. Los valores booleanos se representan con los literales TRUE y FALSE.

Las consultas pueden hacer referencia a los tipos enum de Java especificando el nombre completo de la clase enum. El siguiente ejemplo demuestra el uso de un enum en una expresión condicional, utilizando el enum TipoTelefono:

SELECT e
FROM Empleado e JOIN e.numerosTelefono p
WHERE KEY(p) = com.acme.TipoTelefono.Casa

Un tipo de entidad es simplemente el nombre de entidad de alguna entidad definida y es válido solo cuando se usa con el operador TYPE. Las comillas no se utilizan. Consulte la sección “Herencia y polimorfismo” para ver ejemplos de cuándo usar un literal de tipo de entidad.

Los literales temporales se especifican utilizando la sintaxis de escape JDBC, que define que las llaves encierran el literal. El primer carácter en la secuencia es un “d” o un “t” para indicar que el literal es una fecha o una hora, respectivamente. Si el literal representa una marca de tiempo, se utiliza “ts”. Después del indicador de tipo hay un separador de espacio y luego la fecha real, la hora o la información de la marca de tiempo envuelta entre comillas simples. Las formas generales de los tres tipos de literales temporales, con ejemplos acompañantes, son las siguientes:

  • {d ‘yyyy-mm-dd’} p. ej., {d ‘2009-11-05’}
  • {t ‘hh-mm-ss’} p. ej., {t ‘12-45-52’}
  • {ts ‘yyyy-mm-dd hh-mm-ss.f’} p. ej., {ts ‘2009-11-05 12-45-52.325’}

Toda la información temporal dentro de comillas simples se expresa como dígitos. La parte fraccionaria de la marca de tiempo (la parte “.f”) puede tener varios dígitos y es opcional. Al usar cualquiera de estos literales temporales, recuerde que solo son interpretados por los controladores que admiten la sintaxis de escape JDBC. Normalmente, el proveedor no intentará traducir ni procesar los literales temporales.

3.5.2. Function Expressions

Las expresiones escalares pueden aprovechar funciones que se pueden utilizar para transformar los resultados de la consulta. La Tabla 8-1 resume la sintaxis de cada una de las expresiones de funciones admitidas.

Función Descripción
ABS(numero) Devuelve la versión no firmada del argumento numero. El tipo de resultado es el mismo que el tipo de argumento (entero, float o double).
CONCAT(string1, string2) Devuelve una nueva cadena que es la concatenación de sus argumentos, string1 y string2.
CURRENT_DATE Devuelve la fecha actual según lo definido por el servidor de base de datos.
CURRENT_TIME Devuelve la hora actual según lo definido por el servidor de base de datos.
CURRENT_TIMESTAMP Devuelve la marca de tiempo actual según lo definido por el servidor de base de datos.
INDEX(identification variable) Devuelve la posición de una entidad dentro de una lista ordenada.
EXTRACT(datetime_field FROM datetime_expression) Devuelve el valor del campo o parte correspondiente de la fecha y hora.
LENGTH(string) Devuelve el número de caracteres en el argumento de cadena.
LOCATE(string1, string2 [, start]) Devuelve la posición de string1 en string2, opcionalmente comenzando en la posición indicada por start. El resultado es cero si no se puede encontrar la cadena.
LOWER(string) Devuelve la forma en minúsculas del argumento de cadena.
MOD(number1, number2) Devuelve el módulo de los argumentos numéricos number1 y number2 como un entero.
SIZE(collection) Devuelve el número de elementos en la colección, o cero si la colección está vacía.
SQRT(numero) Devuelve la raíz cuadrada del argumento numérico como un double.
SUBSTRING(string, start, end) Devuelve una parte de la cadena de entrada, comenzando en el índice indicado por start hasta la longitud de los caracteres. Los índices de cadena se miden a partir de uno.
UPPER(string) Devuelve la forma en mayúsculas del argumento de cadena.
TRIM([[LEADING|TRAILING|BOTH] [char] FROM] string) Elimina caracteres iniciales y/o finales de una cadena. Si no se utiliza la palabra clave opcional LEADING, TRAILING o BOTH, se eliminan tanto los caracteres iniciales como los finales. El carácter de recorte predeterminado es el espacio.

La función SIZE requiere atención especial porque es una notación abreviada para una subconsulta agregada. Por ejemplo, considere la siguiente consulta que devuelve todos los departamentos con solo dos empleados:

SELECT d
FROM Departamento d
WHERE SIZE(d.empleados) = 2

Al igual que las expresiones de colección IS EMPTY y MEMBER OF, la función SIZE se traducirá a SQL utilizando una subconsulta. La forma equivalente del ejemplo anterior usando una subconsulta es la siguiente:

SELECT d
FROM Departamento d
WHERE (SELECT COUNT(e)
       FROM d.empleados e) = 2

El caso de uso para la función INDEX puede no ser obvio al principio. Cuando se utilizan colecciones ordenadas, cada elemento de la colección contiene dos piezas de información: el valor almacenado en la colección y su posición numérica dentro de la colección. Las consultas pueden usar la función INDEX para determinar la posición numérica de un elemento en una colección y luego usar ese número con fines de informes o filtrado. Por ejemplo, si los números de teléfono de un empleado se almacenan en orden de prioridad, la siguiente consulta devolvería el primer (y más importante) número para cada empleado:

SELECT e.nombre, p.numero
FROM Empleado e JOIN e.telefonos p
WHERE INDEX(p) = 0

3.5.3. Funciones Nativas de la base de datos: FUNCTION

Uno de los beneficios de Jakarta Persistence QL es que desacopla la aplicación de la base de datos subyacente. Sin embargo, ocasionalmente es necesario utilizar funciones nativas que son propias de la base de datos o definidas por el administrador del sistema. Si bien el uso de estas funciones puede vincular la consulta a la base de datos de destino, aún hay un argumento para utilizar la independencia del mapeo de entidades de Jakarta Persistence QL.

Las funciones de base de datos pueden ser ejecutarse en las consultas de Jakarta Persistence QL a través del uso de la expresión FUNCTION. La palabra clave FUNCTION, seguida del nombre de la función y los argumentos de la función, debe resolverse a un valor escalar que sea aritmético, booleano, de cadena o de un tipo temporal, como fecha, hora o marca de tiempo. Las expresiones FUNCTION pueden ser utilizadas donde sea que los tipos escalares encajen en una expresión, y el tipo de resultado debe coincidir con lo que el resto de la expresión espera. Los argumentos deben ser literales, expresiones que se resuelvan a escalares o parámetros de entrada.

La siguiente consulta invoca una función de base de datos llamada deberiaTenerBonus. El ID del departamento del empleado y los proyectos en los que trabaja se pasan como parámetros, y el tipo de retorno de la función es un booleano. El resultado crea una condición que hace que la consulta devuelva el conjunto de todos los empleados que obtienen un bono:

SELECT DISTINCT e
FROM Empleado e JOIN e.proyectos p
WHERE FUNCTION('deberiaTenerBonus', e.dept.id, p.id)

3.5.4. Expresiones CASE

La expresión CASE de Jakarta Persistence QL es una adaptación de la expresión CASE ANSI SQL-92, teniendo en cuenta las capacidades del lenguaje Jakarta Persistence QL. Las expresiones CASE son herramientas poderosas para introducir lógica condicional en una consulta, con el beneficio de que el resultado de una expresión CASE se puede utilizar en cualquier lugar donde sea válida una expresión escalar.

Las expresiones CASE están disponibles en cuatro formas, dependiendo de la flexibilidad requerida por la consulta. La primera y más flexible forma es la expresión de caso general. Todos los demás tipos de expresión CASE se pueden componer en términos de la expresión de caso general. Tiene la siguiente forma:

CASE {WHEN <cond_expr> THEN <scalar_expr>}+ ELSE <scalar_expr> END

El corazón de la expresión CASE es la cláusula WHEN, de la cual debe haber al menos una. El procesador de consultas resuelve la expresión condicional de cada cláusula WHEN en orden hasta que encuentra una que tenga éxito. Luego evalúa la expresión escalar para esa cláusula WHEN y la devuelve como resultado de la expresión CASE. Si ninguna de las expresiones condicionales de la cláusula WHEN produce un resultado verdadero, se evalúa y devuelve la expresión escalar de la cláusula ELSE. El siguiente ejemplo muestra la expresión de caso general, enumerando el nombre y tipo de cada proyecto que tiene empleados asignados:

SELECT p.nombre,
CASE WHEN TYPE(p) = ProyectoDocumentacion THEN 'Desarrollo'
WHEN TYPE(p) = ProyectoDesarrollo THEN 'Desarrollo'
ELSE 'No-Desarrollo'
END
FROM Proyecto p
WHERE p.empleados IS NOT EMPTY

Observe el uso de la expresión de caso como parte de la cláusula select. Las expresiones CASE son una herramienta poderosa para transformar datos de entidades en consultas de informes.

Una ligera variación en la expresión de caso general es la expresión de caso simple. En lugar de verificar una expresión condicional en cada cláusula WHEN, identifica un valor y resuelve una expresión escalar en cada cláusula WHEN. El primero en coincidir con el valor activa una segunda expresión escalar que se convierte en el valor de la expresión de caso. Tiene la siguiente forma:

CASE <value> {WHEN <scalar_expr1> THEN <scalar_expr2>}+ ELSE <scalar_expr> END

El <value> en esta forma de la expresión es ya sea una expresión de ruta que lleva a un campo de estado o una expresión de tipo para una comparación polimórfica. Podemos simplificar el último ejemplo convirtiéndolo en una expresión de caso simple:

SELECT p.nombre,
CASE TYPE(p)
WHEN ProyectoDocumentacion THEN 'Desarrollo'
WHEN ProyectoDesarrollo THEN 'Desarrollo'
ELSE 'Non-Desarrollo'
END
FROM Proyecto p
WHERE p.empleados IS NOT EMPTY

La tercera forma de la expresión de caso es la expresión coalesce. Esta forma de la expresión de caso acepta una secuencia de una o más expresiones escalares. Tiene la siguiente forma:

COALESCE(<scalar_expr> {,<scalar_expr>}+)

Las expresiones escalares en la expresión COALESCE se resuelven en orden. La primera que devuelve un valor no nulo se convierte en el resultado de la expresión. El siguiente ejemplo demuestra este uso, devolviendo ya sea el nombre descriptivo de cada departamento o el identificador del departamento si no se ha definido ningún nombre:

SELECT COALESCE(d.nombre, d.id)
FROM Departamento d

La cuarta y última forma de la expresión de caso es algo inusual. Acepta dos expresiones escalares y resuelve ambas. Si los resultados de las dos expresiones son iguales, el resultado de la expresión es nulo. De lo contrario, devuelve el resultado de la primera expresión escalar. Esta forma de la expresión de caso se identifica por la palabra clave NULLIF:

NULLIF(<scalar_expr1>, <scalar_expr2>)

Un truco útil con NULLIF es excluir resultados de una función de agregación. Por ejemplo, la siguiente consulta devuelve un recuento de todos los departamentos y un recuento de todos los departamentos que no tienen el nombre ‘QA’:

SELECT COUNT(*), COUNT(NULLIF(d.nombre, 'QA'))
FROM Departamento d

Si el nombre del departamento es ‘QA’, NULLIF devolverá NULL, lo cual luego será ignorado por la función COUNT. Las funciones de agregación ignoran los valores NULL, y se describen más adelante en la sección “Consultas de Agregación”.

3.6. Cláusula ORDER BY

Las consultas pueden ordenarse opcionalmente mediante ORDER BY y una o más expresiones que consisten en variables de identificación, variables de resultado, una expresión de ruta que resuelve a una sola entidad o una expresión de ruta que resuelve a un campo de estado persistente. Las palabras clave opcionales ASC y DESC después de la expresión se pueden usar para indicar órdenes ascendentes o descendentes, respectivamente. El orden de clasificación predeterminado es ascendente.

El siguiente ejemplo demuestra la clasificación por un solo campo:

SELECT e
FROM Empleado e
ORDER BY e.nombre DESC

También se pueden usar múltiples expresiones para refinar el orden de clasificación:

SELECT e, d
FROM Empleado e JOIN e.departamento d
ORDER BY d.nombre, e.nombre DESC

Se puede declarar una variable de resultado en la cláusula SELECT con el propósito de especificar un ítem que se ordenará. Una variable de resultado es efectivamente un alias para su ítem de selección asignado. Ahorra a la cláusula ORDER BY tener que duplicar expresiones de ruta de la cláusula SELECT y permite hacer referencia a ítems de selección calculados e ítems que utilizan funciones de agregación. La siguiente consulta define dos variables de resultado en la cláusula SELECT y luego las utiliza para ordenar los resultados en la cláusula ORDER BY:

SELECT e.nombre, e.salario * 0.05 AS bonus, d.nombre AS nomberDepartamento
FROM Empleado e JOIN e.departamento d
ORDER BY nomberDepartamento, bonus DESC

Si la cláusula SELECT de la consulta utiliza expresiones de ruta de campo de estado, la cláusula ORDER BY se limita a las mismas expresiones de ruta utilizadas en la cláusula SELECT. Por ejemplo, la siguiente consulta no es legal:

SELECT e.nombre
FROM Empleado e
ORDER BY e.salario DESC

Debido a que el tipo de resultado de la consulta es el nombre del empleado, que es de tipo String, el resto de los campos de estado de Empleado ya no están disponibles para ordenar.

4. Consultas de Agregación

Una consulta de agregación es una variación de una consulta select normal. Una consulta de agregación agrupa resultados y aplica funciones de agregación para obtener información resumida sobre los resultados de la consulta. Una consulta se considera una consulta de agregación si utiliza una función de agregación o posee una cláusula GROUP BY y / o una cláusula HAVING. La forma más típica de consulta de agregación implica el uso de una o más expresiones de agrupación y funciones de agregación en la cláusula SELECT emparejadas con expresiones de agrupación en la cláusula GROUP BY. La sintaxis de una consulta de agregación es la siguiente:

SELECT <expresion_seleccionada>
FROM <clausula_from>
[WHERE <expresion_condicional>]
[GROUP BY <clausula_group_by>]
[HAVING <expresion_condicional>]
[ORDER BY <clausula_order_by>]

Las cláusulas SELECT, FROM y WHERE se comportan de manera similar a como se describió anteriormente en las consultas select, con la excepción de algunas restricciones sobre cómo se formula la cláusula SELECT. El poder de una consulta de agregación proviene del uso de funciones de agregación sobre datos agrupados. Considera el siguiente ejemplo de agregación simple:

SELECT AVG(e.salario)
FROM Empleado e

Esta consulta devuelve el salario promedio de todos los empleados de la empresa. AVG es una función de agregación que toma una expresión de ruta de campo de estado numérico como argumento y calcula el promedio sobre el grupo. Como no se especificó ninguna cláusula GROUP BY, el grupo aquí es el conjunto completo de empleados. Ahora considera esta variación, donde el resultado se ha agrupado por el nombre del departamento:

SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUP BY d.nombre

Esta consulta devuelve el nombre de cada departamento y el salario promedio de los empleados en ese departamento. La entidad Departamento se une a la entidad Empleado a través de la relación empleados y luego se forma en un grupo definido por el nombre del departamento. La función AVG luego calcula su resultado en función de los datos de empleados en este grupo. Esto se puede extender aún más para filtrar los datos de modo que los salarios de los gerentes no se incluyan:

SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY
GROUP BY d.nombre

Finalmente, podemos extender esto una última vez para devolver solo los departamentos donde el salario promedio es mayor a $50,000. Considera la siguiente versión de la consulta anterior:

SELECT d.nombre, AVG(e.salario)
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY
GROUP BY d.nombre
HAVING AVG(e.salario) > 50000

Para comprender mejor esta consulta, revisemos los pasos lógicos que se llevaron a cabo para ejecutarla. Las bases de datos utilizan muchas técnicas para optimizar este tipo de consultas, pero conceptualmente se sigue el mismo proceso. Primero, se ejecuta la siguiente consulta sin agrupación:

SELECT d.nombre, e.salario
FROM Departamento d JOIN d.empleados e
WHERE e.directos IS EMPTY

Esto producirá un conjunto de resultados que consiste en todos los pares de nombre de departamento y valor de salario. El motor de consulta luego inicia un nuevo conjunto de resultados y realiza un segundo paso sobre los datos, recopilando todos los valores de salario para cada nombre de departamento y entregándolos a la función AVG. Esta función devuelve el promedio del grupo, que luego se verifica contra los criterios de la cláusula HAVING. Si el valor promedio es mayor que $50,000, el motor de consulta genera una fila de resultados que consiste en el nombre del departamento y el valor promedio del salario.

Las siguientes secciones describen las funciones de agregación disponibles para su uso en consultas de agregación y el uso de las cláusulas GROUP BY y HAVING.

4.1 Funciones de Agregación

AVG

La función AVG toma una expresión de ruta de campo de estado como argumento y calcula el valor promedio de ese campo de estado sobre el grupo. El tipo de campo de estado debe ser numérico y el resultado se devuelve como un Double.

COUNT

La función COUNT toma ya sea una variable de identificación o una expresión de ruta como argumento. Esta expresión de ruta puede resolverse a un campo de estado o un campo de asociación de valor único. El resultado de la función es un valor Long que representa la cantidad de valores en el grupo. El argumento de la función COUNT puede ir precedido opcionalmente por la palabra clave DISTINCT, en cuyo caso se eliminan los valores duplicados antes de contar.

Ejemplo: Contar la cantidad de teléfonos asociados con cada empleado y la cantidad de tipos de números distintos (celular, oficina, hogar, etc.).

SELECT e, COUNT(p), COUNT(DISTINCT p.tipo)
FROM Empleado e JOIN e.telefonos p
GROUP BY e;

MAX

La función MAX toma una expresión de campo de estado como argumento y devuelve el valor máximo en el grupo para ese campo de estado.

MIN

La función MIN toma una expresión de campo de estado como argumento y devuelve el valor mínimo en el grupo para ese campo de estado.

SUM

La función SUM toma una expresión de campo de estado como argumento y calcula la suma de los valores en ese campo de estado sobre el grupo. El tipo de campo de estado debe ser numérico y el tipo de resultado debe corresponder al tipo de campo. Por ejemplo, si se suma un campo Double, el resultado se devolverá como un Double. Si se suma un campo Long, la respuesta se devolverá como un Long.

4.2 Cláusula GROUP BY

La cláusula GROUP BY define las expresiones de agrupación sobre las cuales se agregarán los resultados. Una expresión de agrupación debe ser una expresión de ruta de campo de valor único (campo de estado, embebido que lleva a un campo de estado o campo de asociación de valor único) o una variable de identificación. Si se utiliza una variable de identificación, la entidad no debe tener ningún campo de estado serializado u objetos grandes.

El siguiente ejemplo cuenta la cantidad de empleados en cada departamento:

SELECT d.nombre, COUNT(e)
FROM Departamento d JOIN d.empleados e
GROUP BY d.nombre;

Observa que la misma expresión de campo utilizada en la cláusula SELECT se repite en la cláusula GROUP BY. Todas las expresiones que no son de agregación deben enumerarse de esta manera.

Es posible aplicar más de una función de agregación, como se muestra en el siguiente ejemplo:

SELECT d.nombre, COUNT(e), AVG(e.salario)
FROM Departamento d JOIN d.empleados e
GROUP BY d.nombre;

En esta variación de la consulta, se calcula el salario promedio de todos los empleados en cada departamento además de contar la cantidad de empleados en el departamento.

También se pueden usar múltiples expresiones de agrupación para desglosar aún más los resultados:

SELECT d.nombre, e.salario, COUNT(p)
FROM Departamento d JOIN d.empleados e JOIN e.proyectos p
GROUP BY d.nombre, e.salario;

Ambas expresiones de agrupación, el nombre del departamento y el salario del empleado, deben enumerarse tanto en la cláusula SELECT como en la cláusula GROUP BY. Para cada departamento, esta consulta cuenta la cantidad de proyectos asignados a los empleados según su salario.

En ausencia de una cláusula GROUP BY, las funciones de agregación se aplicarán a todo el conjunto de resultados como un único grupo. Por ejemplo, la siguiente consulta devuelve la cantidad de empleados y su salario promedio en toda la empresa:

SELECT COUNT(e), AVG(e.salario)
FROM Empleado e;

4.3 Cláusula HAVING

La cláusula HAVING define un filtro que se aplicará después de que los resultados de la consulta hayan sido agrupados. Es efectivamente una cláusula WHERE secundaria, y su definición es la misma: la palabra clave HAVING seguida de una expresión condicional. La diferencia clave con la cláusula HAVING es que sus expresiones condicionales están principalmente limitadas a campos de estado o campos de asociación de valor único incluidos en el grupo.

Las expresiones condicionales en la cláusula HAVING también pueden usar funciones de agregación sobre los elementos utilizados para la agrupación, o funciones de agregación que aparecen en la cláusula SELECT. En muchos aspectos, el uso principal de la cláusula HAVING es restringir los resultados basados en los valores de resultado agregados. La siguiente consulta utiliza esta técnica para recuperar todos los empleados asignados a dos o más proyectos:

SELECT e, COUNT(p)
FROM Empleado e JOIN e.proyectos p
GROUP BY e
HAVING COUNT(p) >= 2;

5. Consultas de Actualización

Las consultas de actualización proporcionan un equivalente a la instrucción SQL UPDATE pero con expresiones condicionales de Jakarta Persistence QL. La forma de una consulta de actualización es la siguiente:

UPDATE <nombre de entidad> [[AS] <variable de identificación>]
SET <declaración de actualización> {, <declaración de actualización>}*
[WHERE <expresión condicional>]

Cada declaración UPDATE consiste en una expresión de ruta de valor único, el operador de asignación (=) y una expresión. Las opciones de expresión para la declaración de asignación están ligeramente restringidas en comparación con las expresiones condicionales regulares. El lado derecho de la asignación debe resolverse a un literal, una expresión simple que resuelva a un tipo básico, una expresión de función, una variable de identificación o un parámetro de entrada. El tipo de resultado de esa expresión debe ser compatible con la ruta de asociación simple o el campo de estado persistente en el lado izquierdo de la asignación.

El siguiente ejemplo simple demuestra la consulta de actualización al darle un aumento a $60,000 a los empleados que ganan $55,000:

UPDATE Empleado e
SET e.salario = 60000
WHERE e.salario = 55000;

La cláusula WHERE de una declaración UPDATE funciona de la misma manera que una declaración SELECT y puede utilizar la variable de identificación definida en la cláusula UPDATE en expresiones. Una consulta de actualización ligeramente más compleja pero más realista sería otorgar un aumento de $5,000 a los empleados que trabajaron en un proyecto en particular:

UPDATE Empleado e
SET e.salario = e.salario + 5000
WHERE EXISTS (SELECT p
FROM e.proyectos p
WHERE p.nombre = 'Versión2');

Se pueden modificar más de una propiedad de la entidad objetivo con una sola declaración UPDATE. Por ejemplo, la siguiente consulta actualiza el intercambio telefónico para los empleados en la ciudad de Ottawa y cambia la terminología del tipo de teléfono de Oficina a Negocios:

UPDATE Telefono p
SET p.numero = CONCAT('288', SUBSTRING( p.numero,
LOCATE('-', p.numero), 4)),
p.tipo = 'Business'
WHERE p.empleado.direccion.city = 'Ottawa' AND
p.tipo = 'Oficina';

6. Consultas de borrado

La consulta de eliminación proporciona la misma capacidad que la instrucción SQL DELETE, pero con expresiones condicionales de Jakarta Persistence QL. La forma de una consulta de eliminación es la siguiente:

DELETE FROM <nombre de entidad> [[AS] <variable de identificación>]
[WHERE <condición>]

El siguiente ejemplo elimina a todos los empleados que no están asignados a un departamento:

DELETE FROM Empleado e
WHERE e.departamento IS NULL;

La cláusula WHERE de una declaración DELETE funciona de la misma manera que lo haría para una declaración SELECT. Todas las expresiones condicionales están disponibles para filtrar el conjunto de entidades que se eliminarán. Si no se proporciona la cláusula WHERE, se eliminan todas las entidades del tipo dado.

Las consultas de eliminación son polimórficas. Todas las instancias de subclases de entidades que cumplan con los criterios de la consulta de eliminación también se eliminarán. Sin embargo, las consultas de eliminación no respetan las reglas de cascada. No se eliminarán entidades que no sean del tipo referenciado en la consulta y sus subclases, incluso si la entidad tiene relaciones con otras entidades con eliminaciones en cascada habilitadas.

https://thorben-janssen.com/jpql/

Última actualización: 23.09.2025

11. Paginación de consultas.

1. Paginación de Consultas

Las grandes cantidades de resultados de consultas suelen ser un problema para muchas aplicaciones.

Cuando mostramos el conjunto completo de resultados, por que son muchos, o si el medio de la aplicación hace que mostrar muchas filas sea ineficiente (aplicaciones web, en particular), las aplicaciones deben poder mostrar rangos de un conjunto de resultados y proporcionar a los usuarios la capacidad de controlar el rango de datos que están visualizando.

paginación

La forma más común de esta técnica es presentar al usuario una tabla de tamaño fijo que actúa como una ventana deslizante sobre el conjunto de resultados (en tablas o páginas Web, por ejemplo). Cada incremento de resultados mostrados se llama página, y el proceso de navegar a través de los resultados se llama paginación.

Paginar eficientemente a través de conjuntos de resultados ha sido un desafío tanto para los desarrolladores de aplicaciones como para los proveedores de bases de datos.

Antes de que existiera soporte a nivel de base de datos, una técnica común era recuperar primero todas las claves primarias del conjunto de resultados y luego emitir consultas separadas para obtener los resultados completos utilizando rangos de valores de clave primaria.
Más tarde, los proveedores de bases de datos agregaron el concepto de número de fila lógica a los resultados de las consultas, garantizando que mientras el resultado estuviera ordenado, se podría confiar en el número de fila para recuperar porciones del conjunto de resultados (Ejemplo: SELECT * FROM posts OFFSET 10 LIMIT 10.).
Más recientemente, la especificación JDBC ha llevado esto aún más lejos con el concepto de conjuntos de resultados desplazables, que se pueden navegar hacia adelante y hacia atrás según sea necesario.

La fórmula para el cálculo de la paginación es la siguiente:

OFFSET = (LIMIT * page(N)) — LIMIT

int primerResultado = (nuMeroPágina - 1) * tamanhoPagina;

Paginación Paginación

Las interfaces Query y TypedQuery brindan soporte para la paginación a través de los métodos setFirstResult() y setMaxResults(). Estos métodos especifican el primer resultado que se recibirá (numerado desde cero) y el número máximo de resultados a devolver en relación con ese punto.

Los valores establecidos para estos métodos también se pueden recuperar mediante los métodos getFirstResult() y getMaxResults(). Un proveedor de persistencia puede optar por implementar el soporte para esta característica de varias maneras, ya que no todos los sistemas de bases de datos se benefician del mismo enfoque. Es una buena idea familiarizarse con la forma en que su proveedor aborda la paginación y qué nivel de soporte existe en la plataforma de base de datos objetivo para su aplicación.

Conjunto de resultados en relaciones multivaluadas

Los métodos setFirstResult() y setMaxResults() no deben usarse con consultas que realicen uniones a través de relaciones de colección (uno a muchos y muchos a muchos), porque estas consultas pueden devolver valores duplicados. Los valores duplicados en el conjunto de resultados hacen imposible utilizar una posición de resultado lógica.

En el siguiente código se muestra un ejemplo de paginación. Una vez creado, se inicializa con el nombre de una consulta para contar el total de resultados y el nombre de una consulta para generar el informe. Cuando se solicitan resultados, utiliza el tamaño de página y el número de página actual para calcular los parámetros correctos para los métodos setFirstResult() y setMaxResults(). El número total posible de resultados se calcula ejecutando la consulta de recuento. Usando los métodos next(), previous() y getResultadosActuales(), el código de presentación puede navegar por los resultados según sea necesario. Si este bean se vinculara a una sesión HTTP, podría usarse directamente en una página de Jakarta Server Pages o Jakarta Server Faces que presente los resultados en una tabla de datos.

La clase es una plantilla general para un bean que mantiene el estado intermedio para una consulta de aplicación a partir de la cual los resultados se procesan en segmentos. Se utiliza un bean de sesión con estado.

Paginador de Informes con Estado

@Stateful
public class PaginadorDeResultados {
    @PersistenceContext(unitName="QueryPaging")
    
    private EntityManager em;
    private String nombreConsultaInforme;
    private long paginaActual;
    private long totalResultados;
    private long tamañoPagina;

    public long getTamañoPagina() {
        return tamañoPagina;
    }

    public long getTotalPaginas() {
        return totalResultados / tamañoPagina;
    }

    public void init(long tamañoPagina, String nombreConsultaRecuento, String nombreConsultaInforme) {
        this.tamañoPagina = tamañoPagina;
        this.nombreConsultaInforme = nombreConsultaInforme;
        totalResultados = em.createNamedQuery(nombreConsultaRecuento, Long.class).getSingleResult();
        paginaActual = 0;
    }

    public List getResultadosActuales() {
        return em.createNamedQuery(nombreConsultaInforme)
                .setFirstResult(paginaActual * tamañoPagina)
                .setMaxResults(tamañoPagina)
                .getResultList();
    }

    public void next() {
        paginaActual++;
    }

    public void previous() {
        paginaActual--;
        if (paginaActual < 0) {
            paginaActual = 0;
        }
    }

    public long getPaginaActual() {
        return paginaActual;
    }

    public void setPaginaActual(long paginaActual) {
        this.paginaActual = paginaActual;
    }

    @Remove
    public void finished() {}
}

Ejercicio 11.1. Paginación de películas

Ejercicio. Paginación de películas

Paginación de consultas Paginación de consultas

Características de la base de datos:

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se referencien en el archivo de propiedades, persistence.xml, de modo independiente, como se ha visto en el tema de configuración.

Las tablas de la base de datos y las entidades ya han sido desarrolladas en apartados anteriores.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

Se trata de realizar una aplicación que permita consultar las películas por nombre (pide la introducción de un texto) y muestre las películas de la base de datos de 10 en 10. Se debe mostrar el idPelicula, castelan, orixinal, anoFin y el director (relacionado con PeliculaPersonaxe).

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

  • Crea una clase PeliculaPaginaDTO que tenga los campos idPelicula, castelan, orixinal, anoFin y director.
  • Crea una clase PeliculaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.
Última actualización: 23.09.2025

12. Herencia.

Uno de los errores comunes cometidos por desarrolladores novatos orientados a objetos se abusa del principio de reutilización, llevándolo demasiado lejos, dando lugar a jerarquías de herencia complejas solo por el bien de compartir algunos métodos. Este tipo de jerarquías a menudo conduce a problemas y dificultades a medida que la aplicación se vuelve difícil de depurar y un desafío de mantener.

La mayoría de las aplicaciones disfrutan de los beneficios de al menos alguna herencia en el modelo de objetos. Sin embargo, como con la mayoría de las cosas, se debe aplicar la moderación, especialmente cuando se trata de mapear las clases a bases de datos relacionales.

Grandes jerarquías a menudo pueden llevar a una reducción significativa del rendimiento, y puede ser que el costo de la reutilización de código sea más alto de lo que te gustaría pagar.

Veremos cómo la API de JPA jerarquías de herencia.

1. Jerarquías de Clases

El primer y más obvio lugar para comenzar a hablar sobre herencia es en el modelo de objetos de Java. Después de todo, las entidades son objetos y deberían poder heredar estado y comportamiento de otras entidades. Esto no solo se espera, sino que también es esencial para el desarrollo de aplicaciones orientadas a objetos.

¿Qué significa cuando una entidad hereda estado de su superclase de entidad? Puede implicar cosas diferentes en el modelo de datos, pero en el modelo de Java, simplemente significa que cuando se instancia una entidad de la subclase, tiene su propia versión o copia tanto de su estado localmente definido como de su estado heredado, todo lo cual es persistente. Si bien esta premisa básica no es sorprendente en absoluto, abre la pregunta menos obvia de qué sucede cuando una entidad hereda de algo que no es otra entidad. ¿A qué clases se le permite a una entidad heredar y qué sucede cuando lo hace?

Considera la jerarquía de clases: Empleado - EmpleadoCompañia - EmpladoTiempoCompleto/EmpleadoTiempoParcial:

    public class Empleado {
        private int id;
        private String nombre;
        private Date fechaInicio;
        // ...
    }

    public class EmpleadoContratado extends Empleado {
        private int tarifaDiaria;
        private int plazo;
        // ...
    }

    public class EmpleadoCompania extends Empleado {
        private int vacaciones;
        // ...
    }

    public class EmpleadoTiempoCompleto extends EmpleadoCompania {
        private long salario;
        private long pension;
        // ...
    }

    public class EmpleadoTiempoParcial extends EmpleadoCompania {
        private float tarifaHoraria;
        // ...
    }

El diagrama de clases sería el siguiente:

classDiagram
    class Empleado {
        -int id
        -String nombre
        -Date fechaInicio
    }
    class EmpleadoContratado {
        -int tarifaDiaria
        -int plazo
    }
    class EmpleadoCompania {
        -int vacaciones
    }
    class EmpleadoTiempoCompleto {
        -long salario
        -long pension
    }
    class EmpleadoTiempoParcial {
        -float tarifaHoraria
    }
    Empleado <|-- EmpleadoContratado
    Empleado <|-- EmpleadoCompania
    EmpleadoCompania <|-- EmpleadoTiempoCompleto
    EmpleadoCompania <|-- EmpleadoTiempoParcial

Hay varias formas en que la herencia de clases se puede representar en la base de datos. En el modelo de objetos, puede haber varias formas diferentes de implementar una jerarquía, algunas de las cuales pueden incluir clases que no son entidades.

Diferenciamos entre:

  • Una jerarquía de clases general, que es un conjunto de varios tipos de clases de Java que se extienden entre sí en un árbol.

  • Una jerarquía de entidades, que es un árbol que consiste en clases de entidades persistentes intercaladas con clases que no son entidades. Una jerarquía de entidades tiene su raíz en la primera clase de entidad en la jerarquía.

1.1. Superclases Mapeadas: @MappedSuperclass

https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/mappedsuperclass

La API de Persistencia de Jakarta define un tipo especial de clase llamada superclase mapeada (@MappedSuperclass) que es bastante útil como superclase para entidades.

Una superclase mapeada proporciona una clase útil en la que almacenar estado y comportamiento compartidos que las entidades pueden heredar, pero:

  • En sí misma no es una clase persistente.
  • No puede actuar como una entidad.
  • No se puede consultar.
  • No puede ser el objetivo de una relación.

Anotaciones como @Table no están permitidas en superclases mapeadas porque el estado definido en ellas se aplica solo a sus subclases de entidad.

Las superclases mapeadas se pueden comparar con las entidades de alguna manera similar a como se compara una clase abstracta con una clase concreta; pueden contener estado y comportamiento, pero simplemente no se pueden instanciar como entidades persistentes.

Una clase abstracta solo es útil en relación con sus subclases concretas, y una superclase mapeada es útil solo como estado y comportamiento que se hereda de las subclases de entidad. No juegan un papel en una jerarquía de herencia de entidades aparte de contribuir con ese estado y comportamiento a las entidades que heredan de ellas.

Las superclases mapeadas pueden o no definirse como abstractas en sus definiciones de clase, pero es una buena práctica hacerlas clases Java abstractas reales. No conocemos casos de uso buenos para crear instancias concretas de ellas sin poder persistirlas nunca, y lo más probable es que, si encuentras uno, probablemente quieras que la superclase mapeada sea una entidad.

Todas las reglas de asignación predeterminadas que se aplican a las entidades también se aplican al estado básico y de relación en las superclases mapeadas. La mayor ventaja de usar superclases mapeadas es poder definir un estado compartido parcial que no debería accederse por sí mismo sin el estado adicional que sus subclases de entidad le agregan.

una entidad o una superclase mapeada

Si no estás seguro de si hacer una clase una entidad o una superclase mapeada, solo necesitas preguntarte si alguna vez se precisa realizar consultas o acceder a una instancia que solo se expone como una instancia de esa clase mapeada. Esto también incluye relaciones, ya que una superclase mapeada no se puede usar como el destino de una relación. Si respondes sí a alguna variante de esa pregunta, probablemente deberías hacerla una entidad de primera clase.

Volviendo a la relación anterior, podríamos concebir tratar la clase EmpleadoCompania como una superclase mapeada en lugar de una entidad. Define un estado compartido, pero quizás no tengamos ninguna razón para realizar consultas sobre ella.

Una clase se indica como una superclase mapeada anotándola con la anotación @MappedSuperclass. Los fragmentos de clase siguientes muestran cómo se mapearía la jerarquía con EmpleadoCompania como una superclase mapeada:

@Entity
public class Empleado {
    @Id private int id;
    private String nombre;
    @Temporal(TemporalType.DATE)
    @Column(name="fechaInicio")
    private Date fechaInicio;
    // ...
}

@Entity
public class EmpleadoContratado extends Empleado {
    @Column(name="tarifaDia")
    private int tarifaDiaria;
    private int plazo;
    // ...
}

@MappedSuperclass
public abstract class EmpleadoCompania extends Empleado {
    private int vacaciones;
    // ...
}

@Entity
public class EmpleadoTiempoCompleto extends EmpleadoCompania {
    private long salario;
    private long pension;
    // ...
}

@Entity
public class EmpleadoTiempoParcial extends EmpleadoCompania {
    @Column(name="tarifaHora")
    private float tarifaHoraria;
    // ...
}

1.2. Clases Transitorias en la Jerarquía

Las clases en una jerarquía de entidades, que no son entidades ni superclases mapeadas , se llaman clases transitorias.

Las entidades se pueden heredar clases transitorias ya sea directa o indirectamente a través de una superclase mapeada.

Cuando una entidad hereda de una clase transitoria, el estado definido en la clase transitoria aún se hereda en la entidad, pero no es persistente. En otras palabras, la entidad tendrá espacio asignado para el estado heredado, según las reglas habituales de Java, pero ese estado no será gestionado por el proveedor de persistencia. Se ignorará efectivamente durante el ciclo de vida de la entidad. La entidad podría gestionar ese estado manualmente mediante el uso de métodos de devolución de llamada del ciclo de vida u otros enfoques, pero el estado no se persistirá como parte del ciclo de vida gestionado por el proveedor.

Podría concebirse tener una jerarquía compuesta por una entidad que tiene una subclase transitoria, que a su vez tiene una o más subclases de entidad. Aunque este caso no es realmente común, es posible y se puede lograrse en las raras circunstancias en las que se desee tener un estado transitorio compartido o un comportamiento común. Normalmente, sería más conveniente declarar el estado o comportamiento transitorio en la superclase de entidad que crear una clase transitoria intermedia.

El código muestra una entidad que hereda de una superclase que define un estado transitorio, que es el tiempo en que se creó una entidad en la memoria.

public abstract class EntidadConCache {
    private long tiempoCreacion;
    public EntidadConCache() { tiempoCreacion = System.currentTimeMillis(); }
    public long getTiempoCache() {
        return System.currentTimeMillis() - tiempoCreacion;
    }
}

@Entity
public class Empleado extends EntidadConCache {
    public Empleado() { super(); }
    // ...
}

En este ejemplo, movimos el estado transitorio de la clase de entidad a una superclase transitoria, pero el resultado final es realmente bastante similar. El ejemplo anterior podría haber sido un poco más limpio sin la clase adicional, pero este ejemplo nos permite compartir el estado transitorio y el comportamiento entre cualquier número de entidades que solo necesitan extender EntidadConCache.

1.3. Clases Abstractas y Concretas

Hemos mencionado la noción de clases abstractas frente a concretas en el contexto de superclases mapeadas, pero no entramos en más detalles sobre entidades y clases transitorias. La mayoría de las personas, dependiendo de su filosofía, podrían esperar que todas las clases no hoja en una jerarquía de objetos sean abstractas, o al menos algunas de ellas. Una restricción que obligue a que las entidades siempre sean clases concretas estropearía esto de manera bastante hábil, y afortunadamente este no es el caso.

Es perfectamente aceptable que las entidades, superclases mapeadas o clases transitorias sean abstractas o concretas en cualquier nivel del árbol de herencia. Al igual que con las superclases mapeadas, hacer que las clases transitorias sean concretas en la jerarquía realmente no sirve para ningún propósito y, como regla general, se debe evitar para prevenir errores de desarrollo accidentales y mal uso.

El caso del que no hemos hablado es el de una entidad que es una clase abstracta. La única diferencia entre una entidad que es una clase abstracta y una que es una clase concreta es la regla de Java que prohíbe que las clases abstractas se instancien. Todavía pueden definir estado persistente y comportamiento que será heredado por las subclases de entidad concretas debajo de ellas. Se pueden consultar y el resultado estará compuesto por instancias de subclases de entidad concretas. También pueden llevar los metadatos de asignación de herencia para la jerarquía.

La jerarquía de clases tenía una clase Empleado que era una clase concreta. No querríamos que los usuarios instanciaran accidentalmente esta clase y luego intentaran persistir un empleado parcialmente definido. Podríamos protegernos contra esto definiéndola como abstracta. Luego tendríamos todas nuestras clases no hoja como abstractas y las clases hoja siendo persistentes.


2. Modelos de Herencia

Jakarta Persistence proporciona soporte para tres representaciones de datos diferentes. El uso de dos de ellas es bastante común, mientras que la tercera es menos común y no es necesario admitirla, aunque está completamente definida con la intención de que los proveedores puedan verse obligados a admitirla en el futuro:

  • Una tabla por jerarquía de clases (single-table): una sola tabla que contiene todas las entidades en la jerarquía:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dispositivo_type")
public abstract class Dispositivo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;    
    @Column(name="marca")
    private String marca;

    @Column(name="nombre")
    private String nombre;
    // ...
@Entity
@DiscriminatorValue("ordenador")
public class Ordenador extends Dispositivo {
    private String oS;
    // ...
}

@Entity
@DiscriminatorValue("telefonomovil")
public class TelefonoMovil extends Dispositivo {
    private String color;
    // ...

Dará lugar a una tabla como la siguiente, llamada Dispositivo:

id marca nombre dispositivo_type oS color
1 HP Elite ordenador
2 Apple iPhone telefonomovil
  • Subclases unidas (joined): una tabla por cada clase de entidad concreta en la jerarquía:
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Dispositivo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;    
    @Column(name="marca")
    private String marca;

    @Column(name="nombre")
    private String nombre;
    // ...
@Entity
@PrimaryKeyJoinColumn(name="idOrdenador")
public class Ordenador extends Dispositivo {
    private String oS;
    // ...
}

@Entity
@PrimaryKeyJoinColumn(name="idTelefonoMovil")
public class TelefonoMovil extends Dispositivo {
    private String color;
    // ...
}

En esta estrategia, se crean tablas separadas para la superclase y para cada subclase de entidad concreta, como Ordenador y TelefonoMovil. Cada tabla contiene solo las columnas que son específicas de la subclase, además de las columnas que son heredadas de la superclase.

Sin embargo, los objetos definidos en la superclase no se incluyen en la tabla de la subclase. En su lugar, se crea una relación entre la tabla de la superclase y las tablas de las subclases. Esta relación se establece mediante una clave primaria y una clave foránea.

La clave primaria de la superclase constituye la clase foránea de las subclases. En el ejemplo anterior, la clave primaria de la tabla Dispositivo se convierte en la clave foránea de las tablas Ordenador y TelefonoMovil.

  • Una tabla por clase de entidad concreta (table-per-concrete-class): una tabla por cada clase de entidad concreta y sus superclases.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Dispositivo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;    
    @Column(name="marca")
    private String marca;

    @Column(name="nombre")
    private String nombre;

    @Colum(name="os")
    private String oS;
    // ...
@Entity
public class Ordenador extends Dispositivo {
    private String color;
    // ...
}

Este métdodo es muy similar a MappedSuperclass, pero en este caso, la clase abstracta se convierte en una entidad. Esto significa que la tabla Dispositivo también será creada en la base de datos. Esta estructura de herencia permite crear relaciones con consultas polimórficas y subclases. Sin embargo, cuando se realizan consultas, se escanean todas las tablas de las subclases, lo que puede afectar el rendimiento.

Este método debería ser evitado en los proyectos, ya que puede afectar el rendimiento de la aplicación.


Cuando existe una jerarquía de entidades, siempre tiene su origen en una clase de entidad. Recuerde que las superclases mapeadas no cuentan como niveles en la jerarquía porque solo contribuyen a las entidades debajo de ellas. La clase de entidad raíz debe significar la jerarquía de herencia al estar anotada con la anotación @Inheritance. Esta anotación indica la estrategia que se debe utilizar para el mapeo y debe ser una de las tres estrategias descritas en las siguientes secciones.

Cada entidad en la jerarquía debe definir o heredar su identificador, lo que significa que el identificador debe estar definido ya sea en la entidad raíz o en una superclase mapeada por encima de ella. Una superclase mapeada puede estar más arriba en la jerarquía de clases que donde se define el identificador.

2.1. Estrategia de una Tabla (Single-Table)

La forma más común y eficiente de almacenar el estado de múltiples clases es definir una sola tabla que contenga un conjunto de todas las posibles representaciones de estado en cualquiera de las clases de entidad. Este enfoque se llama estrategia de una tabla (single-table). Tiene la consecuencia de que, para cualquier fila de tabla que represente una instancia de una clase concreta, puede haber columnas que no tengan valores porque se aplican solo a una clase hermana en la jerarquía.

En el esquema vemos que el id se encuentra en la clase de entidad raíz Empleado y es compartido por el resto de las clases de persistencia. Todas las entidades persistentes en un árbol de herencia deben usar el mismo tipo de identificador. No necesitamos pensar mucho en ello antes de ver por qué esto tiene sentido en ambos niveles. En la capa de objetos, no sería posible realizar una operación de búsqueda polimórfica en una superclase si no hubiera un tipo de identificador común que pudiéramos pasar. De manera similar, en la tabla, necesitaríamos varias columnas de clave primaria, pero sin poder completarlas todas en cualquier inserción dada de una instancia que solo hiciera uso de una de ellas.

La tabla debe contener suficientes columnas para almacenar todo el estado en todas las clases. Una fila individual almacena el estado de una instancia de una entidad de tipo entidad concreta, lo que normalmente implicaría que algunas columnas quedarían sin completar en cada fila. Por supuesto, esto lleva a la conclusión de que las columnas mapeadas al estado de la subclase concreta deben ser nulas, lo cual normalmente no es un gran problema pero podría ser un problema para algunos administradores de bases de datos.

En general, el enfoque de una sola tabla tiende a desperdiciar más espacio en la tabla de la base de datos, pero ofrece un rendimiento máximo tanto para consultas polimórficas como para operaciones de escritura. El SQL necesario para realizar estas operaciones es simple, está optimizado y no requiere uniones.

Para especificar la estrategia de una sola tabla para la jerarquía de herencia, la clase de entidad raíz se anota con la anotación @Inheritance con su estrategia configurada en SINGLE_TABLE. En nuestro modelo anterior, esto significaría anotar la clase Empleado de la siguiente manera:

@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
public abstract class Empleado { ... }

Resulta que la estrategia de una sola tabla es la predeterminada, por lo que ni siquiera necesitaríamos incluir el elemento de estrategia. Una anotación @Inheritance vacía haría el mismo efecto.

En el esquema vemos la representación de una sola tabla de nuestro modelo de jerarquía Empleado. En términos de estructura de tabla y arquitectura de esquema para la estrategia de una sola tabla, no importa si EmpleadoCompania es una superclase mapeada o una entidad.

Columna discriminante

Se ha creado una columna adicional llamada Empleado_TYPE que no se asignó a ningún campo en ninguna de las clases. Este campo tiene un propósito especial y es necesario al utilizar una sola tabla para modelar la herencia. Se llama columna discriminadora y se asigna mediante la anotación @DiscriminatorColumn en conjunción con la anotación @Inheritance que ya hemos aprendido.

El elemento name especifica el nombre de la columna que se debe usar como columna discriminadora, y si no se especifica, se utilizará por defecto una columna llamada DTYPE. Un elemento discriminatorType dicta el tipo de la columna discriminadora. Algunas aplicaciones prefieren usar cadenas para discriminar entre los tipos de entidad, mientras que a otras les gusta usar valores enteros para indicar la clase.

El tipo de la columna discriminadora puede ser uno de los tres tipos de columna discriminadora predefinidos: INTEGER, STRING o CHAR. Si no se especifica el elemento discriminatorType, entonces se asumirá el tipo predeterminado STRING.

Valor discriminante

Cada fila en la tabla tendrá un valor en la columna discriminadora llamado valor discriminador, o un indicador de clase, para indicar el tipo de entidad que se almacena en esa fila. Por lo tanto, cada entidad concreta en la jerarquía de herencia necesita un valor discriminador específico para ese tipo de entidad para que el proveedor pueda procesar o asignar el tipo de entidad correcto cuando carga y almacena la fila.

La forma de hacer esto es mediante la anotación @DiscriminatorValue en cada clase de entidad concreta. El valor de cadena en la anotación especifica el valor discriminador que se asignará a las instancias de la clase cuando se inserten en la base de datos. Esto permitirá que el proveedor reconozca las instancias de la clase cuando emite consultas. Este valor debe ser del mismo tipo que se especificó o se predeterminó como el elemento discriminatorType en la anotación @DiscriminatorColumn.

Si no se especifica la anotación @DiscriminatorValue, entonces el proveedor utilizará una forma específica del proveedor para obtener el valor:

  • Si discriminatorType era STRING, entonces el proveedor simplemente usará el nombre de la entidad como la cadena indicadora de la clase.
  • Si discriminatorType es INTEGER, entonces tendríamos que especificar los valores discriminadores para cada clase de entidad o ninguno de ellos. Si especificáramos algunos pero no otros, no podríamos garantizar que un valor generado por el proveedor no se superponga con uno que hayamos especificado.

El Listado muestra cómo se asigna nuestra jerarquía de Empleado a una estrategia de una sola tabla.

@Entity
@Table(name="EMP")
@Inheritance
@DiscriminatorColumn(name="EMP_TYPE")
public abstract class Empleado { ... }

@Entity
public class EmpleadoContratado extends Empleado { ... }

@MappedSuperclass
public abstract class EmpleadoCompania extends Empleado { ... }

@Entity
@DiscriminatorValue("FTEmp")
public class EmpleadoTiempoCompleto extends EmpleadoCompania { ... }

@Entity(name="PTEmp")
public class EmpleadoTiempoParcial extends EmpleadoCompania { ... }

La clase Empleado es la clase raíz, por lo que establece la estrategia de herencia y la columna discriminadora. Hemos asumido la estrategia predeterminada de SINGLE_TABLE y el tipo de discriminador STRING.

Ni las clases Empleado ni EmpleadoCompania tienen valores discriminadores, porque los valores discriminadores no deben especificarse para clases de entidad abstractas, superclases mapeadas, clases transitorias o cualquier clase abstracta en ese sentido. Solo las clases de entidad concretas usan valores discriminadores, ya que son las únicas que realmente se almacenan y recuperan de la base de datos.

La entidad EmpleadoContratado no utiliza una anotación @DiscriminatorValue, porque la cadena predeterminada “EmpleadoContratado”, que es el nombre de entidad predeterminado que se le da a la clase, es justo lo que queremos. La clase EmpleadoTiempoCompleto lista explícitamente su valor discriminador como “FTEmp”, para que sea lo que se almacene en cada fila para las instancias de EmpleadoTiempoCompleto. Mientras tanto, la clase EmpleadoTiempoParcial obtendrá “PTEmp” como su valor discriminador porque estableció su nombre de entidad en “PTEmp”, y el nombre de entidad se utiliza como el valor discriminador cuando no se especifica ninguno.

Podemos ver una muestra de algunos de los datos que podríamos encontrar dadas la configuración y el modelo anteriores. Podemos ver desde la columna discriminadora EMP_TYPE que hay tres tipos diferentes de entidades concretas. También vemos valores nulos en las columnas que no se aplican a una instancia de entidad.

2.2 Estrategia de Herencia Unida (Joined Strategy)

Desde la perspectiva de un desarrollador de Java, un modelo de datos que mapea cada entidad a su propia tabla tiene mucho sentido. Cada entidad, ya sea abstracta o concreta, tendrá su estado mapeado a una tabla diferente. Consistente con nuestra descripción anterior, las superclases mapeadas no se mapean a sus propias tablas, sino que se mapean como parte de sus subclases de entidad.

Mapear una tabla por entidad proporciona la reutilización de datos que ofrece un esquema de datos normalizado y es la forma más eficiente de almacenar datos compartidos por múltiples subclases en una jerarquía. El problema es que, cuando llega el momento de volver a ensamblar una instancia de cualquiera de las subclases, las tablas de las subclases deben unirse con las tablas de la superclase. Esto hace bastante obvio por qué esta estrategia se llama estrategia unida. También es algo más costoso insertar una instancia de entidad, porque se debe insertar una fila en cada una de sus tablas de superclase en el camino.

Recuerde que, según la estrategia de una sola tabla, el identificador debe ser del mismo tipo para cada clase en la jerarquía. En un enfoque unido, tendremos el mismo tipo de clave primaria en cada una de las tablas, y la clave primaria de una tabla de subclase también actúa como una clave externa que se une a su tabla de superclase. Esto debería sonar familiar debido a su similitud con el caso de múltiples tablas anterior en el capítulo, donde uníamos las tablas usando las claves primarias de las tablas y usábamos la anotación @PrimaryKeyJoinColumn para indicarlo. Usamos esta misma anotación en el caso de herencia unida, ya que tenemos múltiples tablas que cada una contiene el mismo tipo de clave primaria y cada una potencialmente tiene una fila que contribuye al estado final combinado de la entidad.

Aunque la herencia unida es intuitiva y eficiente en términos de almacenamiento de datos, las uniones que requiere la hacen algo costosa de usar cuando las jerarquías son profundas o extensas. Cuanto más profunda sea la jerarquía, más uniones se requerirán para ensamblar instancias de la entidad concreta en la parte inferior. Cuanto más amplia sea la jerarquía, más uniones se requerirán para hacer consultas en una superclase de entidad.

En la gráfica vemos nuestro ejemplo de Empleado mapeado a una arquitectura de tabla unida. Los datos de una subclase de entidad se distribuyen en las tablas de la misma manera que se distribuyen en la jerarquía de clases. Cuando se utiliza una arquitectura unida, la decisión de si EmpleadoCompania es una superclase mapeada o una entidad marca la diferencia, ya que las superclases mapeadas no se asignan a tablas. Una entidad, incluso si es una clase abstracta, siempre lo hace. La gráfica lo muestra como una superclase mapeada, pero si fuera una entidad, entonces existiría una tabla adicional COMPANY_EMP con columnas ID y VACATION, y la columna VACATION en las tablas FT_EMP y PT_EMP no estaría presente.

Para mapear una jerarquía de entidades a un modelo unido, la anotación @Inheritance solo necesita especificar JOINED como la estrategia. Al igual que en el ejemplo de una sola tabla, las subclases adoptarán la misma estrategia que se especifica en la superclase de entidad raíz.

Aunque hay múltiples tablas para modelar la jerarquía, la columna discriminadora solo se define en la tabla raíz, por lo que la anotación @DiscriminatorColumn se coloca en la misma clase que la anotación @Inheritance.

Consejo

Algunos proveedores ofrecen implementaciones de herencia unida sin el uso de una columna discriminadora. Se deben usar columnas discriminadoras si se requiere portabilidad del proveedor.

Nuestro ejemplo de jerarquía Empleado se puede mapear utilizando el enfoque unido que se muestra en el Listado. En este ejemplo, utilizamos columnas discriminadoras de tipo entero en lugar del tipo de cadena predeterminado.

Jerarquía de Entidades Mapeada Usando la Estrategia Unida

@Entity
@Table(name="EMP")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn( name="EMP_TYPE",
discriminatorType=DiscriminatorType.INTEGER)
public abstract class Empleado { ... }

@Entity
@Table(name="CONTRACT_EMP")
@DiscriminatorValue("1")
public class EmpleadoContratado extends Empleado { ... }

@MappedSuperclass
public abstract class EmpleadoCompania extends Empleado { ... }

@Entity
@Table(name="FT_EMP")
@DiscriminatorValue("2")
public class EmpleadoTiempoCompleto extends EmpleadoCompania { ... }

@Entity
@Table(name="PT_EMP")
@DiscriminatorValue("3")
public class EmpleadoTiempoParcial extends EmpleadoCompania { ... }

2.3. Estrategia de una Tabla por Clase Concreta (Table-per-Concrete-Class Strategy)

Un tercer enfoque para mapear una jerarquía de entidades es utilizar una estrategia donde se define una tabla por clase concreta. Esta arquitectura de datos va en la dirección opuesta a la no normalización de los datos de entidad y asigna cada clase de entidad concreta y todo su estado heredado a una tabla separada. Esto tiene el efecto de hacer que todo el estado compartido se redefina en las tablas de todas las entidades concretas que lo heredan. Este enfoque no es necesario ser admitido por los proveedores, pero se incluye porque se anticipa que será necesario en una versión futura de la API. Lo describimos brevemente por completitud.

El aspecto negativo de usar esta estrategia es que hace que las consultas polimórficas a lo largo de una jerarquía de clases sean más costosas que las otras estrategias. El problema es que debe emitir varias consultas separadas a través de cada una de las tablas de subclases o consultar todas ellas usando una operación UNION, que generalmente se considera costosa cuando hay mucha cantidad de datos. Si hay clases concretas que no son hojas, entonces cada una de ellas tendrá su propia tabla. Las subclases de las clases concretas tendrán que almacenar los campos heredados en sus propias tablas, junto con sus propios campos definidos

.

El lado positivo de las jerarquías de tablas por clase concreta, en comparación con las jerarquías unidas, se ve en casos de consulta sobre instancias de una sola entidad concreta. En el caso unido, cada consulta requiere una unión, incluso cuando se consulta a través de una sola clase de entidad concreta. En el caso de tabla por clase concreta, es similar a la jerarquía de una sola tabla porque la consulta se limita a una sola tabla. Otra ventaja es que desaparece la columna discriminadora. Cada entidad concreta tiene su propia tabla separada, y no hay mezcla ni compartición de esquema, por lo que nunca se necesita un indicador de clase.

Mapear nuestro ejemplo a este tipo de jerarquía es cuestión de especificar la estrategia como TABLE_PER_CLASS y asegurarse de que haya una tabla para cada una de las clases concretas. Si se está utilizando una base de datos heredada, entonces las columnas heredadas podrían tener nombres diferentes en cada una de las tablas concretas, y la anotación @AttributeOverride sería útil. En este caso, la tabla CONTRACT_EMP no tenía las columnas NAME y fechaInicio, sino que en su lugar tenía FULLNAME y SDATE para los campos name y fechaInicio definidos en Empleado. Si el atributo que queríamos anular fuera una asociación en lugar de un mapeo de estado simple, aún podríamos anular el mapeo, pero necesitaríamos usar la anotación @AssociationOverride en lugar de @AttributeOverride. La anotación @AssociationOverride nos permite anular las columnas de unión utilizadas para hacer referencia a la entidad objetivo de una asociación de muchos a uno o de uno a uno definida en una superclase mapeada. Para mostrar esto, necesitamos agregar un atributo manager a la superclase mapeada EmpleadoCompania. La columna de unión se asigna por defecto en la clase EmpleadoCompania a la columna MANAGER en las dos tablas de subclases FT_EMP y PT_EMP, pero en PT_EMP el nombre de la columna de unión es en realidad MGR. Anulamos la columna de unión agregando la anotación @AssociationOverride a la clase EmpleadoTiempoParcial y especificando el nombre del atributo que estamos anulando y la columna de unión que estamos anulando. El Listado muestra un ejemplo completo de los mapeos de entidades, incluidas las anulaciones.

Jerarquía de Entidades Mapeada Usando una Estrategia de Tabla por Clase Concreta

@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public abstract class Empleado {
@Id private int id;
private String name;
@Temporal(TemporalType.DATE)
@Column(name="fechaInicio")
private Date fechaInicio;
// ...
}

@Entity
@Table(name="CONTRACT_EMP")
@AttributeOverride(name="name", column=@Column(name="FULLNAME"))
@AttributeOverride(name="fechaInicio", column=@Column(name="SDATE"))
public class EmpleadoContratado extends Empleado {
@Column(name="tarifaDia")
private int tarifaDiaria;
private int term;
// ...
}

@MappedSuperclass
public abstract class EmpleadoCompania extends Empleado {
private int vacaciones;
@ManyToOne
private Empleado manager;
// ...
}

@Entity @Table(name="FT_EMP")
public class EmpleadoTiempoCompleto extends EmpleadoCompania {
private long salary;
@Column(name="PENSION")
private long pensionContribution;
// ...
}

@Entity
@Table(name="PT_EMP")
@AssociationOverride(name="manager",
joinColumns=@JoinColumn(name="MGR"))
public class EmpleadoTiempoParcial extends EmpleadoCompania {
@Column(name="tarifaHora")
private float tarifaHoraria;
// ...
}

La organización de la tabla muestra cómo se asignan estas columnas a las tablas concretas. Consulta el esquema para obtener una imagen clara de cómo se verían las tablas y cómo se almacenarían los diferentes tipos de instancias de empleados.

3. Herencia Mixta

Debemos comenzar esta sección diciendo que la práctica de mezclar tipos de herencia dentro de una sola jerarquía de herencia actualmente está fuera de la especificación. Lo estamos incluyendo porque es útil e interesante, pero ofrecemos una advertencia de que podría no ser portátil depender de dicho comportamiento, incluso si su proveedor lo admite.

Además, realmente tiene sentido mezclar solo tipos de herencia de una sola tabla y herencia unida. Mostramos un ejemplo de mezcla de estos dos, teniendo en cuenta que el soporte para ellos depende del proveedor. La intención es que, en futuras versiones de la especificación, los casos más útiles se estandaricen y se requiera que las implementaciones compatibles los admitan.

La premisa para mezclar tipos de herencia es que es muy posible que un modelo de datos incluya una combinación de diseños de una sola tabla y tablas unidas dentro de una sola jerarquía de entidades. Esto se puede ilustrar tomando nuestro ejemplo unido en el esquema y almacenando las instancias de EmpleadoTiempoCompleto y EmpleadoTiempoParcial en una sola tabla. Esto produciría un modelo como el que se muestra.

En este ejemplo, se utiliza la estrategia unida para las clases Empleado y EmpleadoContratado, mientras que las clases EmpleadoCompania, EmpleadoTiempoCompleto y EmpleadoTiempoParcial vuelven a un modelo de una sola tabla. Para hacer este cambio de estrategia de herencia en el nivel de EmpleadoCompania, necesitamos realizar un cambio simple en la jerarquía. Necesitamos convertir EmpleadoCompania en una entidad abstracta en lugar de una superclase mapeada para que pueda llevar los nuevos metadatos de herencia. Tenga en cuenta que esto es simplemente un cambio de anotación, sin realizar ningún cambio en el modelo de dominio.

Las estrategias de herencia se pueden mapear como se muestra en el Listado. Observe que no necesitamos tener una columna discriminadora para la subjerarquía de una sola tabla, ya que ya tenemos una en la tabla superior EMP.

Listado. Jerarquía de Entidades Mapeada Usando Estrategias Mixtas

@Entity
@Table(name="EMP")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="EMP_TYPE")
public abstract class Empleado {
    @Id private int id;
    private String name;
    @Temporal(TemporalType.DATE)
    @Column(name="fechaInicio")
    private Date fechaInicio;
    // ...
}

@Entity
@Table(name="CONTRACT_EMP")
public class EmpleadoContratado extends Empleado {
    @Column(name="tarifaDia") private int tarifaDiaria;
    private int term;
    // ...
}

@Entity
@Table(name="COMPANY_EMP")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
public abstract class EmpleadoCompania extends Empleado {
    private int vacaciones;
    // ...
}

@Entity
public class EmpleadoTiempoCompleto extends EmpleadoCompania {
    private long salary;
    @Column(name="PENSION")
    private long pensionContribution;
    // ...
}

@Entity
public class EmpleadoTiempoParcial extends EmpleadoCompania {
    @Column(name="tarifaHora")
    private float tarifaHoraria;
    // ...
}

Ejercicio

Implementa:

  • Cancion. Clase que representa una canción, con: idCancion (Long), titulo (String), autor (String), duración (int), dataPublicacion (LocalDate).
  • MediaSong. Hereda de Cancion y, a mayores, tiene el audio, guardado como byte[].
  • PlayList. Contiene la lista de Reproducibles, en este caso, de tipo MediaSong, así como idPlayList (Long), nome (String) y dataCreacion (LocalDate).
  • Reproducible, IPlayList, PlayListObserver: interfaces que deben implantar, aquellas clases que sean reproducibles, una PlayList o un observador de PlayList respectivamente (no las precisáis). Crea las relaciones necesarias entre las clases para ajustarse a los parámetros de la base de datos.

Especificación JPA

1. Herencia

Una entidad puede heredar de otra clase de entidad.

Las entidades admiten herencia, asociaciones polimórficas y consultas polimórficas.

Tanto las clases abstractas como las clases concretas pueden ser entidades. Ambas pueden llevar la anotación @Entity, mapearse como entidades y consultarse como entidades.

Las entidades pueden extender clases que no son entidades y viceversa.

1.1. Clases de Entidad Abstractas

Se puede especificar una clase abstracta como entidad. Una entidad abstracta difiere de una entidad concreta solo en que no se puede instanciar directamente. Se mapea como una entidad y puede ser el objetivo de consultas (que operarán y/o recuperarán instancias de sus subclases concretas).

Una clase de entidad abstracta se anota con la anotación @Entity (o se designa en el descriptor XML como una entidad, que no veremos).

El siguiente ejemplo muestra el uso de una clase de entidad abstracta en la jerarquía de herencia de entidades.

Ejemplo: Clase abstracta como entidad

Ojo con las mayúsculas y minúsculas en los nombres de las tablas. Por defecto, el nombre de la tabla es el nombre de la clase en mayúsculas, pero esto depende del proveedor JDBC y si usamos H2, si hemos puesto la propiedad DATABASE_TO_UPPER=FALSE o hibernate.implicit_naming_strategy en true o false.

@Entity
@Table(name="Empleado")
@Inheritance(strategy=JOINED) // Por defecto es SINGLE_TABLE. JOINED es la estrategia de subclases unidas
public abstract class Empleado {
    @Id
    protected Integer idEmpleado;

    @Version
    protected Integer version;

    @ManyToOne
    protected Direccion direccion;

    // ...
}

@Entity
@Table(name="EmpleadoTC") // Por defecto es EmpleadoTiempoCompleto (en mayúsculas, depende del proveedor JDBC)
@DiscriminatorValue("TC")
@PrimaryKeyJoinColumn(name="IdEmpleadoTC") // Por defecto es idEmpleado
public class EmpleadoTiempoCompleto extends Empleado {
    // Hereda idEmpleado, pero se mapea en esta clase como  EmpleadoTC.IdEmpleadoTC
    // Hereda version mapeada a Empleado.version
    // Hereda dirección mapeada a Empleado.direccion fk

    // Por defecto a EmpleadoTC.salario
    protected Integer salario;

    // ...
}

@Entity
@Table(name="EmpleadoTP") // Por defecto es EmpleadoTiempoParcial (en mayuscúlas, depende del proveedor JDBC)
@DiscriminatorValue("TP")
// La columna PK es EmpleadoTP.idEmpleado debido a la clave foránea predeterminada heredada de Empleado
public class EmpleadoTiempoParcial extends Empleado {
    protected Float salarioPorHora;

    // ...
}

1.2. Superclases Mapeadas

Una entidad puede heredar de una superclase que proporciona estado de entidad persistente e información de mapeo, pero que no es en sí misma una entidad.

Por lo general, el propósito de dicha superclase mapeada es definir información de estado y mapeo que es común a múltiples clases de entidades.

Una superclase mapeada, a diferencia de una entidad, no es consultable y no debe pasarse como argumento a las operaciones de EntityManager o Query. Las relaciones persistentes definidas por una superclase mapeada deben ser unidireccionales.

Se pueden especificar tanto clases abstractas como clases concretas como superclases mapeadas. Se utiliza la anotación MappedSuperclass (o el elemento de descriptor XML mapped-superclass) para designar una superclase mapeada.

Una clase designada como superclase mapeada no tiene una tabla separada definida para ella. Su información de mapeo se aplica a las entidades que heredan de ella.

Una clase designada como superclase mapeada se puede mapear de la misma manera que una entidad, excepto que los mapeos se aplicarán solo a sus subclases, ya que no existe una tabla para la superclase mapeada en sí. Cuando se aplican a las subclases, los mapeos heredados se aplicarán en el contexto de las tablas de las subclases. La información de mapeo se puede anular en dichas subclases utilizando las anotaciones AttributeOverride y AssociationOverride o los elementos XML correspondientes.

Todos los demás valores predeterminados de mapeo de entidad se aplican igualmente a una clase designada como superclase mapeada.

El siguiente ejemplo ilustra la definición de una clase concreta como una superclase mapeada.

Ejemplo: Clase concreta como superclase mapeada

@MappedSuperclass
public class Empleado {
    @Id
    protected Integer idEmpleado;

    @Version
    protected Integer version;

    @ManyToOne
    @JoinColumn(name="ADDR")
    protected Direccion direccion;

    public Integer getIdEmpleado() { ... }

    public void setIdEmpleado(Integer id) { ... }

    public Direccion getDireccion() { ... }

    public void setDireccion(Direccion addr) { ... }
}

// La tabla predeterminada es la tabla EmpleadoTiempoCompleto
@Entity
public class EmpleadoTiempoCompleto extends Empleado {
    // Campo idEmpleado heredado mapeado a EmpleadoTiempoCompleto.IDEMPLEADO
    // Campo versión heredado mapeado a EmpleadoTiempoCompleto.VERSION
    // Campo dirección heredado mape

ado a EmpleadoTiempoCompleto.ADDR fk

    // Por defecto a EmpleadoTiempoCompleto.SALARIO
    protected Integer salario;

    public EmpleadoTiempoCompleto() {}

    public Integer getSalario() { ... }

    public void setSalario(Integer salario) { ... }
}

@Entity
@Table(name="PT_EMP")
@AssociationOverride(name="direccion", joincolumns=@JoinColumn(name="ADDR_ID"))
public class EmpleadoTiempoParcial extends Empleado {
    // Campo idEmpleado heredado mapeado a PT_EMP.IDEMPLEADO
    // Campo versión heredado mapeado a PT_EMP.VERSION
    // Mapeo de campo dirección anulado a PT_EMP.ADDR_ID fk
    @Column(name="SALARIO_POR_HORA")
    protected Float salarioPorHora;

    public EmpleadoTiempoParcial() {}

    public Float getSalarioPorHora() { ... }

    public void setSalarioPorHora(Float salario) { ... }
}

1.3. Clases no Entidad en la Jerarquía de Herencia de Entidades

Una entidad puede tener una superclase no entidad, que puede ser una clase concreta o abstracta.

La superclase no entidad sirve únicamente para la herencia de comportamiento. El estado de una superclase no entidad no es persistente. Cualquier estado heredado de superclases no entidad no es persistente en una clase de entidad heredera. Este estado no persistente no es gestionado por el administrador de entidades. Se ignoran cualquier anotación en tales superclases.

Las clases no entidad no se pueden pasar como argumentos a los métodos de las interfaces EntityManager o Query y no pueden llevar información de mapeo.

El siguiente ejemplo ilustra el uso de una clase no entidad como superclase de una entidad.

Ejemplo: Superclase no entidad

public class Carrito {
    protected Integer contadorOperaciones; // estado transitorio

    public Carrito() {
        contadorOperaciones = 0;
    }

    public Integer getContadorOperaciones() {
        return contadorOperaciones;
    }

    public void incrementarContadorOperaciones() {
        contadorOperaciones++;
    }
}

@Entity
public class CarritoDeCompras extends Carrito {
    Collection<Item> items = new Vector<Item>();

    public CarritoDeCompras() {
        super();
    }

    // ...

    @OneToMany
    public Collection<Item> getItems() {
        return items;
    }

    public void addItem(Item item) {
        items.add(item);
        incrementarContadorOperaciones();
    }
}

2. Estrategias de Mapeo de Herencia

El mapeo de jerarquías de clases se especifica a través de metadatos.

Hay tres estrategias básicas que se utilizan al mapear una clase o jerarquía de clases a una base de datos relacional:

  1. Una tabla única por jerarquía de clases.
  2. Una estrategia de subclases unidas, en la que los campos específicos de una subclase se asignan a una tabla separada que los campos comunes de la clase principal, y se realiza una unión para instanciar la subclase.
  3. Una tabla por clase de entidad concreta.

Se requiere que una implementación admita la estrategia de una tabla única por jerarquía de clases y la estrategia de subclases unidas.

La compatibilidad con la estrategia de tabla por clase de entidad concreta es opcional en esta versión. Las aplicaciones que utilicen esta estrategia de mapeo no serán portátiles.

No se requiere soporte para la combinación de estrategias de herencia dentro de una sola jerarquía de herencia de entidades según esta especificación.

2.1. Estrategia de Una Tabla por Jerarquía de Clases

En esta estrategia, todas las clases de una jerarquía se asignan a una sola tabla. La tabla tiene una columna que sirve como “columna de discriminador”, es decir, una columna cuyo valor identifica la subclase específica a la que pertenece la instancia representada por la fila.

Esta estrategia de mapeo proporciona un buen soporte para relaciones polimórficas entre entidades y para consultas que abarcan la jerarquía de clases.

Sin embargo, tiene la desventaja de que requiere que las columnas que corresponden al estado específico de las subclases sean nulas.

2.2. Estrategia de Subclases Unidas

En la estrategia de subclases unidas, la raíz de la jerarquía de clases se representa mediante una sola tabla. Cada subclase se representa mediante una tabla separada que contiene aquellos campos que son específicos de la subclase (no heredados de su superclase), así como la(s) columna(s) que representan su clave primaria. La(s) columna(s) de la clave primaria de la tabla de la subclase sirve como clave foránea a la clave primaria de la tabla de la superclase.

Esta estrategia proporciona soporte para relaciones polimórficas entre entidades.

Tiene la desventaja de que requiere que se realice una o más operaciones de unión para instanciar instancias de una subclase. En jerarquías de clases profundas, esto puede conducir a un rendimiento inaceptable. Las consultas que abarcan la jerarquía de clases también requieren uniones.

2.3. Estrategia de Una Tabla por Clase de Entidad Concreta

En esta estrategia de mapeo, cada clase se asigna a una tabla separada. Todas las propiedades de la clase, incluidas las propiedades heredadas, se asignan a columnas de la tabla de la clase.

Esta estrategia tiene las siguientes desventajas:

  1. Proporciona un soporte deficiente para relaciones polimórficas.
  2. Por lo general, requiere que se emitan consultas UNION SQL (o una consulta SQL separada por subclase) para consultas que están destinadas a abarcar la jerarquía de clases.
Última actualización: 23.09.2025

Ejercicios de la unidad de JPA

Ejercicios de apuntes

Ejercicio 01.01. Creación de un proyecto con JPA

Para crear un proyecto con JPA y Hibernate, se puede utilizar el asistente de creación de proyectos de Eclipse o IntelliJ IDEA; sin embargo, con la versión Community de IntelliJ IDEA no se puede crear un proyecto con JPA a través del asistente. Crea un proyecto Java Maven y añade las dependencias de Hibernate y la API de Jakarta Persistence.

Ejercicio 01.02. Creación de un archivo de configuración de persistencia

Crea un directorio META-INF en el directorio src/main/resources y añade un archivo persistence.xml con la configuración de la unidad de persistencia con el nombre com.sanclemente.ad.jpa.exemplo.

El fichero de configuración persistence.xml debe apuntar a una base de datos H2 en memoria. Además, debes añadir los Drivers de H2 para que la aplicación pueda conectarse a la base de datos:

<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>2.3.232</version>
</dependency>

Ten en cuenta que precisas crear la base de datos en memoria H2 y añadir las tablas necesarias, por lo que el parámetro jakarta.persistence.schema-generation.database.action debe ser “create”.

Ejercicio 01.03. Creación de una entidad

Crea una entidad Estudiante con idEstudiante (Long), nombre, apellidos, fechaDeNacimiento y dirección. Añade los atributos necesarios y las anotaciones para que sea una entidad. La clave primaria será idEstudiante de tipo autoincremental.

Ejercicio 01.04. Creación de una entidad

Crea una clase AppEstudiante que se conecte a la base de datos y añada un estudiante a la tabla de la base de datos.

Aunque lo veremos más adelante, lo que precisamos es crear un gestor de entidades e invocar al método persist para añadir un estudiante a la base de datos:

public class AppEstudiante {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("com.sanclemente.ad.jpa.exemplo");
        EntityManager em = emf.createEntityManager();

        Estudiante estudiante = new Estudiante("Juan", "Pérez", LocalDate.of(2000, 1, 1), "Calle Mayor, 1");

        em.getTransaction().begin();
        em.persist(estudiante);
        em.getTransaction().commit();
        
        // IMprime el estudiante para ver si se ha añadido correctamente y tiene un id

        em.close();
        emf.close();
    }
}

Para recuperarlo precisamos invocar al método find del gestor de entidades:

Estudiante estudiante = em.find(Estudiante.class, 1L); // Recupera el estudiante con id 1

Ejercicio 03.01. Creación de una aplicación de persistencia de una biblioteca

Queremos desarrollar una aplicación para una biblioteca y necesitamos interactuar con una base de datos que contiene información sobre los libros que tenemos en nuestra colección.

Para ello, vamos a crear una clase Book que represente la entidad libro, la clase Contido y otra clase BookDAO que nos permita realizar operaciones básicas CRUD (Create, Read, Update y Delete) sobre la tabla Book en la base de datos.

Además, precisamos una clase BibliotecaJpaManager para la gestión y obtención de los objetos de tipo EntityManagerFactory de una manera eficiente. Emplearemos el patrón Singleton para el gestor BibliotecaJpaManager, que tenga un único objeto de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos (queremos que el objeto de tipo EntityManagerFactory sea único para cada unidad de persistencia, para cada unidad de persistencia, no así el EntityManager, que podrá hacer varios para cada unidad de persistencia).

A) BASE DE DATOS (es la misma base de datos que hemos empleado en la unidad de bases de datos con JDBC):

Está formada por una tabla Book y una tabla Contido. La tabla Book tiene una estructura SIMILAR a la siguiente:

Columna Tipo de dato Descripción
idBook int Identificador único del ejemplar del libro
isbn varchar(13) Identificador del libro
titulo varchar(100) Título del libro
autor varchar(100) Autor del libro
anho int Año de publicación del libro
disponible boolean Indica si el libro está disponible
portada Blob Portada del libro en formato binario
dataPublicacion Date Fecha de publicación del libro
-- PUBLIC.Book definition
-- Drop table
-- DROP TABLE PUBLIC.Book;
CREATE TABLE PUBLIC.Book (
	idBook INTEGER NOT NULL AUTO_INCREMENT,
	isbn CHARACTER VARYING(13) NOT NULL,
	titulo CHARACTER VARYING(255) NOT NULL,
	autor CHARACTER VARYING(255),
	anho INTEGER,
	disponible BOOLEAN DEFAULT TRUE,
	portada BINARY LARGE OBJECT,
	dataPublicacion DATE,
	CONSTRAINT BOOK_PK PRIMARY KEY (idBook)
);
CREATE UNIQUE INDEX IdBookPK ON PUBLIC.Book (idBook);
CREATE INDEX IdxBookISBN ON PUBLIC.Book (isbn);
CREATE INDEX IdxBookTitle ON PUBLIC.Book (titulo);
CREATE UNIQUE INDEX PRIMARY_KEY_93 ON PUBLIC.Book (idBook);

La tabla Contido tiene una estructura SIMILAR a la siguiente:

Columna Tipo de dato Descripción
idContido int Identificador único del contenido del libro
idBook int Identificador del libro
contido Blob Contenido del libro en formato binario

*idBook es una clave foránea+ que referencia a la tabla Book.

-- PUBLIC.Contido definition
-- Drop table
-- DROP TABLE PUBLIC.Contido;

CREATE TABLE PUBLIC.Contido (
	idContido INTEGER NOT NULL AUTO_INCREMENT,
	idBook INTEGER NOT NULL,
	contido CHARACTER LARGE OBJECT,
	CONSTRAINT Contido_PK PRIMARY KEY (idContido)
);
CREATE INDEX FK_ID_BOOK_INDEX_9 ON PUBLIC.Contido (idBook);
CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC.Contido (idContido);

-- PUBLIC.Contido foreign keys
ALTER TABLE PUBLIC.Contido ADD CONSTRAINT FK_ID_BOOK FOREIGN KEY (idBook) REFERENCES PUBLIC.Book(idBook) ON DELETE CASCADE ON UPDATE CASCADE;

Parámetros de la base de datos:

DRIVER: "org.h2.Driver"
URL: "jdbc:h2:rutaBaseDatosSinExtensión;DB_CLOSE_ON_EXIT=TRUE;FILE_LOCK=NO;DATABASE_TO_UPPER=FALSE"

El fichero persistencia.xml debe tener la siguiente configuración:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="bibliotecaH2" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>-->
        <exclude-unlisted-classes>false</exclude-unlisted-classes> <-- false si no se listan las clases en el archivo de configuración -->
        <properties>
            <!--      <property name="jakarta.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/peliculas"/>-->
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:rutaALaBaseDeDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/>
            <!-- Ejemplo con Access -->
            <!--<property name="jakarta.persistence.jdbc.url" value="jdbc:ucanaccess://rutabase_base_datos.mdb"/>-->
            <!--      <property name="jakarta.persistence.jdbc.user" value="root"/>-->
            <!--      <property name="jakarta.persistence.jdbc.password" value=""/>-->
            <property name="jakarta.persistence.jdbc.user" value=""/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <!--      <property name="jakarta.persistence.jdbc.driver" value="net.ucanaccess.jdbc.UcanaccessDriver"/>-->
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <!-- Automáticamente, genera el esquema de la base de datos -->
            <property name="jakarta.persistence.schema-generation.database.action" value="none"/>

            <!-- Muestra por pantalla las sentencias SQL -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.highlight_sql" value="true"/>
            <!--      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />--> <!-- para HSQLDB y Ucanaccess -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        </properties>
    </persistence-unit>
</persistence>

B) Clase BibliotecaJpaManager:

Mediante el patrón Singleton crea una clase BibliotecaJpaManager, mediante el patrón Singleton de manera que tenga un atributo emFactory de tipo EntityManagerFactory y que nos permita obtener un objeto de tipo EntityManager para realizar las operaciones sobre la base de datos.

Además, debe tener un método estático getEntityManager que devuelva un objeto de tipo EntityManager y que se encargue de crear el objeto EntityManager.

Hazlo con Thread-Safe y doble comprobación.

Reto: haz que la clase BibliotecaJpaManager tenga un singleton para cada factory, guardándolos en un mapa con el nombre de la unidad de persistencia como clave:

private static Map<String, EntityManagerFactory> instancies = new HashMap<>();

C) Clase Book implementa Serializable:

Haz que sea una entidad JPA y que implemente la interfaz Serializable.

La clase Book debe tener los siguientes atributos:

  • idBook: Long (autonumérico)
  • isbn: String (tamaño 13)
  • title: String
  • author: String
  • ano: Integer
  • available: Boolean
  • portada: byte[]
  • dataPublicacion: LocalDate (Nuevo campo)
  • List<Contido> contenido; (Nuevo, lista de contenidos del libro, de momento, mientras no tengamos relaciones, hazlo transient)

(Fíjate que ya no existe el campo contido[] que habíamos definido en la clase Book de la unidad de bases de datos con JDBC).

La clase debe tener, al menos, los siguientes constructores:

  • Book()
  • Book(String isbn, String title, String author, Short year, Boolean available, byte[] portada)
  • Book(Long idBook, String isbn, String title, String author, Short year, Boolean available, byte[] portada)
  • Aquellos que consideres necesarios.

La lista de Contido es una lista de objetos de tipo Contido que representan los contenidos del libro. La clase Contido tiene los siguientes atributos: idContido y contido. Ten en cuenta que existe en la base de datos una tabla Contido con los campos idContido y contido y una referencia al libro mediante una clave foránea idBook. De momento, no incluyas la List de contenidos en la clase Book, hazlos transient (bien con la anotación @Transient o con la palabra reservada transient), hasta que veamos las relaciones, que será @OneToMany.

Los métodos “set” de las propiedades deben devolver una referencia al propio objeto para poder encadenarlos.

IMPORTANTE: ten en cuenta que los atributos de la clase Book no coinciden con los campos de tabla por lo que debes refactorizar: author -> autor, ano -> anho, avaliable -> disponible, … o emplear la anotación @Column para mapear los atributos de la clase con los campos de la tabla.

Métodos de la clase Book (ya implantados):

  • Get y set para cada atributo.

  • setPortada (sin implantar): recibe File y lo asigna al atributo portada.

  • setPortada (sin implantar): recibe un array de bytes y lo asigna al atributo portada.

  • setPortada (Sin implantar): recibe un String con el nombre del fichero y lo asigna al atributo portada.

  • getImage: devuelve un objeto de tipo Image con la portada del libro.

    public Image getImage() {
    if (portada != null) {
        try (ByteArrayInputStream bis = new ByteArrayInputStream(portada)) {
            return ImageIO.read(bis);
        } catch (IOException e) {
        }
    }
    return null;
}
  • equals y hashCode: considerando que son iguales cuando tienen el mismo isbn. Además, el método hashCode debe devolver un valor coherente con el método equals (todos los objetos iguales deben tener, al menos el mismo hashCode).

  • toString: devuelve el título, el autor y el año. Si no está disponible escribe un asterisco.

D) Clase Contido implementa Serializable:

A diferencia de la clase empleada en la unidad de bases de datos con JDBC, la clase Contido no debe tener referencia al idBook, pues no es la mejor práctica (está hecho sólo a modo de ejemplo), debe tener, si queremos la relación bidireccional, una referencia a Book.

  • idContido: Long (autonumérico)
  • contido: String (contenido del libro en formato texto). Puedes hacer un atributo de tipo String o byte[] (para almacenar el contenido en formato binario), en cualquier caso, deberías modificar la tabla Contido en la base de datos.
  • Book book (relación con la clase Book)

Si has implantado la clase ContidoDao, debes modificar los métodos que obtienen el idBook del book:

contido.getBook().getIdBook();

E) Clase BookJPADao:

Esta clase, al igual que la clase BookDao, la clase BookJPADaodebe implantar la interface Dao<T>, de modo que tenga un objeto de tipo EntityManagercomo atributo. En sistemas empresariales, como la gestión de transacciones no se suele hacer por método, se guarda una referencia a la clase EntityManagerFactory y se gestiona por medio de try-with-resources para manejar los cierres de los EntityManager.

Dao<T>:

import java.util.List;

/**
 *
 * @author pepecalo
 * @param <T> Tipo de dato del objeto
 */
public interface DAO<T> {

    T get(long id);

    List<T> getAll();

    void save(T t);

    void update(T t);
   
    void delete(T t);

    public boolean deleteById(long id);

    public List<Integer> getAllIds();

    public void updateLOB(T book, String f); // en BookJPADao recibe un objeto de tipo Book y un String con el nombre del fichero

    public void updateLOBById(long id, String f);
    
    void deleteAll();
}

Clase BookJPADao:

Implementa la interfaz DAO<Book> y gestiona las operaciones CRUD sobre la tabla Book de la base de datos. Tiene como atributo un objeto de tipo EntityManager que recoge en el constructor.

Clase BookDAOFactory:

Factory de clases que implanten la interfaz DAO<Book>.

import jakarta.persistence.EntityManager;

/**
 * Factory de clases que implanten la interfaz DAO<Book>.
 * 
 * @version 1.0
 * @since 1.0
 * @see BookJpaDAO
 * @see TipoDAO
 */

public class BookDaoFactory {
    
    public enum TipoDao {
        JDBC_H2, JPA_H2, JPA_POSTGRES, HIBERNATE, JSON, JDBC_POSTGRES;
    }

    public static Dao<Book> getBookDAO(TipoDao tipo) {
        switch (tipo) {
            // ..
        }
        return null;
    }
}

Implementa un método estático getBookDAO que recoge el tipo de DAO que se va a emplear y devuelve el objeto de tipo BookJPADAO. Sería interesante hacer cambios para que getBookDao recoja los parámetros necesarios como propiedades de la base de datos, nombre del archivo JSON, nombre de la unidad de persistencia, etc.

public static Dao<Book> getBookDAO(TipoDao tipo, Map<String, String> propiedades) {
    switch (tipo) {
        case JPA_H2:
            return new BookJPADao(BibliotecaJpaManager.getEntityManager(propiedades.get("unidadPersistencia")));
        // ...
        default:
            return null;
    }
    
}

AppBiblioteca:

Ejecuta la aplicación para que haga uso del BookDaoFactory para obtener un objeto de tipo DAO<Book> para asignarlo al controlador de la aplicación. La aplicación debe funcionar igual que con JDBC, pero ahora con JPA.

Con JDBC_H2:

Dao<Book> bookDao = BookDaoFactory.getBookDAO(BookDaoFactory.TipoDAO.JDBC_H2);

Con JPA_H2:

Dao<Book> bookDao = BookDaoFactory.getBookDAO(BookDaoFactory.TipoDAO.JPA_H2);

Haz pruebas con los dos tipos de DAO. ¿Has notado alguna diferencia? Haz mejoras sobre el funcionamiento de la aplicación.

Puedes hacer pruebas de persistencia de libros en la base de datos:

Book libro = new Book("9788424937744", "Tractatus logico-philosophicus-investigaciones filosóficas", "Ludwig Wittgenstein", 2017, false);
libro = new Book("9788499088150", "Verano", "J. M. Coetzee", 2011, true);

03.01. Solución

Solución: BibliotecaJpaManager
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;
import java.util.Map;

import static com.javhoz.ad.biblioteca.model.BibliotecaLogger.LOG;

public class BibliotecaJpaManager {

    public static final String BIBLIOTECA_H2 = "bibliotecaH2";
    public static final String BIBLIOTECA_POSTGRES = "bibliotecaPostgres";


    private static final Map<String, EntityManagerFactory> instancies = new HashMap<>();

    private BibliotecaJpaManager() {
    }

    private static boolean isEntityManagerFactoryClosed(String unidadPersistencia) {
        return !instancies.containsKey(unidadPersistencia) || instancies.get(unidadPersistencia) == null ||
                !instancies.get(unidadPersistencia).isOpen();
    }

    public static EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        if (isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (BibliotecaJpaManager.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    try {
                        instancies.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                    } catch (Exception e) {
                        LOG.error("Erro ó crear a unidade de persistencia " + unidadPersistencia +
                                ": " + e.getMessage());
                    }
                }
            }
        }
        return instancies.get(unidadPersistencia);
    }


    public static EntityManager getEntityManager(String persistenceUnitName) {
        return getEntityManagerFactory(persistenceUnitName).createEntityManager();
    }


    public static void close(String persistenceUnitName) {
        if (instancies.containsKey(persistenceUnitName)) {
            instancies.get(persistenceUnitName).close();
            instancies.remove(persistenceUnitName);
        }
    }

}
Solución: Book
import jakarta.persistence.*;

import javax.imageio.ImageIO;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.util.Objects;

/**
 * @author pepecalo
 */
@Entity
public class Book implements Serializable {

    //    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idBook;
    @Column(length = 13, nullable = false, unique = true))
    private String isbn;
    @Column(name = "titulo", nullable = false)
    private String title;
    @Column(name = "autor")
    private String author;
    @Column(name = "anho")
    private Short ano;
    @Column(name = "disponible")
    private Boolean available;
    private byte[] portada;

    private LocalDate dataPublicacion;

//    @Transient // Ambas opciones son válidas
    transient private List<Contido> contenido = new ArrayList<>();

    private static final long serialVersionUID = 1L;

    public Book() {
    }

    public Book(String title, String author, Short year, Boolean available) {
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
    }

    public Book(String isbn, String title, String author, Short year,
                Boolean available) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
    }

    public Book(String isbn, String title, String author, Short year,
                Boolean available, byte[] portada) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
        this.portada = portada;
    }

    public Book(Long idBook, String isbn, String title, String author,
                Short year, Boolean available, byte[] portada) {
        this.idBook = idBook;
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.ano = year;
        this.available = available;
        this.portada = portada;
    }

    public Long getIdBook() {
        return idBook;
    }

    public Book setIdBook(Long idBook) {
        this.idBook = idBook;
        return this;
    }

    public String getIsbn() {
        return isbn;
    }

    public Book setIsbn(String isbn) {
        this.isbn = isbn;
        return this;
    }

    public String getTitle() {
        return title;
    }

    public Book setTitle(String title) {
        this.title = title;
        return this;
    }

    public String getAuthor() {
        return author;
    }

    public Book setAuthor(String author) {
        this.author = author;
        return this;
    }

    public Short getYear() {
        return ano;
    }

    public Book setAno(Short ano) {
        this.ano = ano;
        return this;
    }

    public Boolean isAvailable() {
        return available;
    }

    public Book setAvailable(Boolean available) {
        this.available = available;
        return this;
    }

    public byte[] getCover() {
        return portada;
    }

    public Book setCover(byte[] portada) {
        this.portada = portada;
        return this;
    }

    public LocalDate getDataPublicacion() {
        return dataPublicacion;
    }

    public Book setDataPublicacion(LocalDate dataPublicacion) {
        this.dataPublicacion = dataPublicacion;
        return this;
    }

    /**
     * Asigna la portada con flujos, leyendo los bytes.
     *
     * @param f
     */
    public Book setPortada(File f) {
        if (f == null || !f.exists())
            return this;
        Path p = Paths.get(f.getAbsolutePath());
        try (BufferedInputStream bi = new BufferedInputStream(Files.newInputStream(p));
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesLidos;
            while ((bytesLidos = bi.read(buffer)) > 0) {
                outputStream.write(buffer, 0, bytesLidos);
            }

            portada = outputStream.toByteArray();
        } catch (FileNotFoundException ex) {
            System.err.println("Archivo no encontrado: " + ex.getMessage());
        } catch (IOException ex) {
            System.err.println("Erro de E/S: " + ex.getMessage());
        }
        return this;
    }

    /**
     * Asigna la portada con Java NIO, leyendo los bytes.
     *
     * @param file
     */
    public Book setPortada(String file) {
        try {
            Path ruta = Paths.get(file);
            portada = Files.readAllBytes(ruta);
        } catch (IOException ex) {
            System.err.println("Error de E/S: " + ex.getMessage());
        }
        return this;
    }

    public Image getImage() {
        if (portada != null) {
            try (ByteArrayInputStream bis = new ByteArrayInputStream(portada)) {
                Image imaxe = ImageIO.read(bis);
                if(available) {
                    imaxe.getGraphics().drawLine(0,0, 100, 100);
                }

                return imaxe;
            } catch (IOException e) {
            }
        }
        return null;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 41 * hash + Objects.hashCode(this.isbn);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final Book other = (Book) obj;
        return Objects.equals(this.isbn, other.isbn);
    }

    @Override
    public String toString() {
        return idBook + "] [isbn: " + isbn + "] " + title + ". "
                + author + " (" + ano + ") [" + ((available) ? '*' : ' ') + ']';
    }

}
Solución: Clase Contido

De momento, no hemos declarado Contido como entidad JPA, pero lo haremos en el futuro, cuando veamos las relaciones.

import java.util.Objects;

/**
 * @autor pepecalo
 * CREATE TABLE PUBLIC.Contido (
 * 	idContido INTEGER NOT NULL AUTO_INCREMENT,
 * 	idBook INTEGER NOT NULL,
 * 	contido CHARACTER LARGE OBJECT,
 * 	CONSTRAINT Contido_PK PRIMARY KEY (idContido),
 * 	CONSTRAINT FK_ID_BOOK FOREIGN KEY (idBook) REFERENCES PUBLIC.Book(idBook) ON DELETE CASCADE ON UPDATE CASCADE
 * );
 * CREATE UNIQUE INDEX PRIMARY_KEY_9 ON PUBLIC.Contido (idContido);
 */

public class Contido {

    private Long idContido;
    private String contido;
    private Book book;

    public Contido() {
    }

    public Contido(Long idBook, String contido) {
        this.contido = contido;
    }

    public Contido(Long idContido, Long idBook) {
        this.idContido = idContido;
    }

    public Contido(Long idContido, Long idBook, String contido) {
        this.idContido = idContido;
        this.contido = contido;
    }

    public Long getIdContido() {
        return idContido;
    }

    public void setIdContido(Long idContido) {
        this.idContido = idContido;
    }

    public String getContido() {
        return contido;
    }

    public void setContido(String contido) {
        this.contido = contido;
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    @Override
    public int hashCode() {
        return 97 * 7 + Objects.hashCode(this.idContido);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Contido other)) return false;
        return Objects.equals(this.idContido, other.idContido);
    }

    @Override
    public String toString() {
        return idContido + ": " + contido;
    }
}
Solución: BookJPADao
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.TypedQuery;

import java.util.List;

public class BookJPADao implements Dao<Book> {

    private final EntityManager em;

    public BookJPADao(EntityManager em) {
        this.em = em;
    }

    @Override
    public Book get(long id) {
        return em.find(Book.class, id);
    }

    @Override
    public List<Book> getAll() {
        TypedQuery<Book> query = em.createQuery("SELECT b FROM Book b", Book.class);
        return query.getResultList();
    }

    @Override
    public void save(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.persist(book);
        tx.commit();
    }

    @Override
    public void update(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.merge(book);
        tx.commit();
    }

    @Override
    public void delete(Book book) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.remove(book);
        tx.commit();
    }

    @Override
    public boolean deleteById(long id) {
        Book book = get(id);
        if (book != null) {
            delete(book);
            return true;
        }
        return false;
    }

    @Override
    public List<Integer> getAllIds() {
        TypedQuery<Integer> query = em.createQuery("SELECT b.idBook FROM Book b", Integer.class);
        return query.getResultList();
    }

    @Override
    public void updateLOB(Book book, String f) {
        book.setPortada(f);
        update(book); // La tansacción se hace en el método update
    }

    @Override
    public void updateLOBById(long id, String f) {
        Book book = get(id);
        if (book != null) {
            updateLOB(book, f); // La transacción se hace en el método updateLOB, que a su vez llama a update
        }
    }

    @Override
    public void deleteAll() {
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        em.createQuery("DELETE FROM Book").executeUpdate();
        tx.commit();
    }
}
Solución: BookDaoFactory
import java.util.Map;

public class BookDaoFactory {
    public enum TipoDAO {
        JDBC_H2, JPA_H2, JPA_POSTGRES, HIBERNATE, JSON, JDBC_POSTGRES;
    }

    public static Dao<Book> getBookDAO(TipoDAO tipo) {
        switch (tipo) {
            case JDBC_H2:
                BibliotecaConnectionMaganer bibliotecaConnection = BibliotecaConnectionMaganer.getInstance();
                return new BookDao(bibliotecaConnection.getConnection());
            case JPA_H2:
                return new BookJPADao((BibliotecaJpaManager.getEntityManager(BibliotecaJpaManager.BIBLIOTECA_H2)));
                // ...
        }
        return null;
    }
}

Ejercicio 04.01. Descarga y creación de la base de datos de JokeAPI

Dado el modelo de la aplicación de JokeAPI, en la que tenemos las enumeraciones Categoriam TipoChiste, Flag y la clase Chiste, vamos a crear una base de datos con JPA y los chistes de la API.

Enumeraciones

A) La enumeración Categoria tiene los siguientes valores:

public enum Categoria {
    ANY("Any"),
    MISC("Misc"),
    PROGRAMMING("Programming"),
    DARK("Dark"),
    PUN("Pun"),
    SPOOKY("Spooky"),
    CHRISTMAS("Christmas");
    //...
}
Detalle de implementación de la enumeración Categoría
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * <p>
 * Enumeración de categorías de chistes.
 * Pueden ser: Any, Misc, Programming, Dark, Pun, Spooky, Christmas
 * Atributo: nombre, de tipo cadena.
 */
public enum Categoria {
    ANY("Any"),
    MISC("Misc"),
    PROGRAMMING("Programming"),
    DARK("Dark"),
    PUN("Pun"),
    SPOOKY("Spooky"),
    CHRISTMAS("Christmas");

    private final String nombre;

    Categoria(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve la categoría a partir de su nombre.
     *
     * @param nombre Nombre de la categoría
     * @return Categoría
     */
    public static Categoria getCategoria(String nombre) {
        for (Categoria c : Categoria.values()) {
            if (c.getNombre().equals(nombre)) {
                return c;
            }
        }
        return null;
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre de la categoría.
     *
     * @return Nombre de la categoría
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }

}

B) La enumeración TipoChiste contiene los siguientes valores:

public enum TipoChiste {
    SINGLE("single"),
    TWOPART("twopart");
    //...
}
Detalle de implementación de la enumeración TipoChiste
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * Enumeración de tipos de chistes.
 * Pueden ser: single, twopart
 * Atributo: String nombre.
 * Constructor: TipoChiste(String nombre)
 * @see Categoria
 * @see Flag
 * @see Chiste
 *
 */
public enum TipoChiste {
    SINGLE("single"),
    TWOPART("twopart");

    private final String nombre;

    TipoChiste(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve el tipo de chiste a partir de su nombre.
     * @param nombre Nombre del tipo de chiste
     * @return Tipo de chiste
     */
    public static TipoChiste getTipoChiste(String nombre) {
        for (TipoChiste tc : TipoChiste.values()) {
            if (tc.getNombre().equals(nombre)) {
                return tc;
            }
        }
        return null;
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre del tipo de chiste.
     * @return Nombre del tipo de chiste
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }
}

C) La enumeración Flag contiene los siguientes valores:

Flag es una enumeración con los siguientes valores:

```java
public enum Flag {
    EXPLICIT("Explicit"),
    NSFW("NSFW"),
    RELIGION("Religion"),
    POLITICAL("Political"),
    RACIST("Racist"),
    SEXIST("Sexist");
    //...
}
Detalle de implementación de la enumeración Flag
package com.javhoz.ad.chistes.model;

/**
 * Updated by javhoz on 16/01/2025.
 * Enumeración de banderas de chistes.
 * Pueden ser: NSFW, RELIGION, POLITICAL, RACIST, SEXIST
 * Atributo: String nombre.
 * Constructor: Flag(String nombre)
 * @see Categoria
 * @link <a href="https://v2.jokeapi.dev/flags">https://v2.jokeapi.dev/flags</a>
 */
public enum Flag {
    EXPLICIT("Explicit"),
    NSFW("NSFW"),
    RELIGION("Religion"),
    POLITICAL("Political"),
    RACIST("Racist"),
    SEXIST("Sexist");

    private final String nombre;

    Flag(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    /**
     * Devuelve la bandera a partir de su nombre.
     * @param nombre Nombre de la bandera
     * @return Bandera
     */
    public static Flag getFlag(String nombre) {
        // Con expresiones lambda:
        return java.util.Arrays.stream(Flag.values()).filter(f -> f.getNombre().equals(nombre)).findFirst()
                .orElse(null);
/*        // Con un bucle for:
//        for (Flag f : Flag.values()) {
//            if (f.getNombre().equals(nombre)) {
//                return f;
//            }
//        }
//        return null;
        */
    }

    /**
     * Sobreescribe el método toString() para que devuelva el nombre de la bandera.
     * @return Nombre de la bandera
     * @see java.lang.Enum#toString()
     */
    @Override
    public String toString() {
        return nombre;
    }
}

D) Lenguaje es una enumeración con los siguientes valores:

public enum Lenguaje {
    CS("cs"),
    DE("de"),
    EN("en"),
    ES("es"),
    FR("fr"),
    PT("pt");
    //...
}
Detalle de implementación de la enumeración Lenguaje
package com.javhoz.ad.chistes.model;

import java.util.Arrays;

/**
 * Lenguajes admitidos por la API de chistes.
 *  * "jokeLanguages": [
 *  *         "cs",
 *  *         "de",
 *  *         "en",
 *  *         "es",
 *  *         "fr",
 *  *         "pt"
 *  *     ]
 *  Atributo con el nombre del lenguaje del chiste.
 *
 * @see <a href="https://sv443.net/jokeapi/v2/#languages">https://sv443.net/jokeapi/v2/#languages</a>
 */
public enum Lenguaje {
    CS("cs"),
    DE("de"),
    EN("en"),
    ES("es"),
    FR("fr"),
    PT("pt");

    private final String lenguaje;

    /**
     * Constructor de la clase Lenguajes.
     * @param lenguaje Nombre del lenguaje
     */
    Lenguaje(String lenguaje) {
        this.lenguaje = lenguaje;
    }

    /**
     * Devuelve el nombre del lenguaje.
     * @return Nombre del lenguaje
     */
    public String getLenguaje() {
        return lenguaje;
    }

    public static Lenguaje getLenguaje(String lenguaje) {
        // Con expresiones lambda:
        return Arrays.stream(Lenguaje.values()).filter(l -> l.getLenguaje().equals(lenguaje)).findFirst()
                .orElse(null);
        /* // Con un bucle for:
//        for (Lenguaje l : Lenguaje.values()) {
//            if (l.getLenguaje().equals(lenguaje)) {
//                return l;
//            }
//        }
//         return null;
*/

    }
    
    @Override
    public String toString() {
        return lenguaje;
    }
}

Clases

A) La clase Chiste tiene los siguientes atributos:

public class Chiste {
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
    //...
}
Detalle de implementación de la clase Chiste
package com.javhoz.ad.chistes.model;

import jakarta.persistence.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Updated by javhoz on 16/01/2025.
 * <p>
 * Clase que representa un chiste.
 * Atributos: id de tipo int, categoria de tipo Categoria, idiomade tipo Lenguaje, tipo de TipoChiste,
 *  List<Flag> banderas, String chiste, String respuesta.
 */
public class Chiste {
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;

    /**
     * Constructor de la clase Chiste.
     * @param id Identificador del chiste
     * @param categoria Categoría del chiste
     * @param idioma Idioma del chiste
     * @param tipo Tipo del chiste
     * @param chiste Chiste
     * @param respuesta Respuesta del chiste
     */
    public Chiste(int id, Categoria categoria, String idioma, TipoChiste tipo, String chiste, String respuesta) {
        this.id = id;
        this.categoria = categoria;
        this.tipo = tipo;
        this.chiste = chiste;
        this.respuesta = respuesta;
        this.banderas = new ArrayList<>();
        this.lenguaje = Lenguaje.getLenguaje(idioma);
    }

    /**
     * Constructor por defecto de la clase Chiste.
     *
     */
    public Chiste() {
//        this.id = 0;
        this.categoria = Categoria.ANY;
        this.lenguaje = Lenguaje.EN;
        this.tipo = TipoChiste.SINGLE;
        this.chiste = "";
        this.respuesta = "";
        this.banderas = new ArrayList<>();
    }

    /**
     * Devuelve el identificador del chiste.
     * @return Identificador del chiste
     */
    public int getId() {
        return id;
    }

    /**
     * Establece el identificador del chiste.
     * @param id Identificador del chiste
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * Devuelve la categoría del chiste.
     * @return Categoría del chiste
     */
    public Categoria getCategoria() {
        return categoria;
    }

    public String getCategoriaString() {
        return categoria.getNombre();
    }

    /**
     * Establece la categoría del chiste.
     * @param categoria Categoría del chiste
     */
    public void setCategoria(Categoria categoria) {
        this.categoria = categoria;
    }

    public void setCategoria(String categoria) {
        this.categoria = Categoria.getCategoria(categoria);
    }
    
    public Lenguaje getLenguaje() {
        return lenguaje;
    }

    public String getLenguajeString() {
        return lenguaje.getLenguaje();
    }

    public void setLenguaje(String lenguaje) {
        this.lenguaje = Lenguaje.getLenguaje(lenguaje);
    }

    public void setLenguaje(Lenguaje lenguaje) {
        this.lenguaje = lenguaje;
    }
    
    /**
     * Devuelve el tipo del chiste.
     * @return Tipo del chiste
     */
    public TipoChiste getTipo() {
        return tipo;
    }

    public String getTipoString() {
        return tipo.getNombre();
    }

    /**
     * Establece el tipo del chiste.
     * @param tipo Tipo del chiste
     */
    public void setTipo(TipoChiste tipo) {
        this.tipo = tipo;
    }

    public void setTipo(String tipo) {
        this.tipo = TipoChiste.getTipoChiste(tipo);
    }

    /**
     * Devuelve las banderas del chiste.
     * @return Banderas del chiste
     */
    public List<Flag> getBanderas() {
        return banderas;
    }

    /**
     * Añade una bandera al chiste.
     * @param flag Bandera a añadir
     */
    public void addFlag(Flag flag) {
        banderas.add(flag);
    }

    public boolean removeFlag(Flag bandera) {
        return banderas.remove(bandera);
    }

    /**
     * Si el chiste tiene esa bandera, devuelve true.
     * @param bandera Bandera a comprobar
     * @return  true si el chiste tiene esa bandera, false en caso contrario
     */
    public boolean containsFlag(Flag bandera) {
        return banderas.contains(bandera);
    }
    
    /**
     * Devuelve el chiste como cadena de caracteres.
     * @return Chiste como String
     */
    public String getChiste() {
        return chiste;
    }

    /**
     * Establece el chiste.
     * @param chiste Chiste
     */
    public void setChiste(String chiste) {
        this.chiste = chiste;
    }

    /**
     * Devuelve la respuesta del chiste.
     * @return Respuesta del chiste
     */
    public String getRespuesta() {
        return respuesta;
    }

    /**
     * Establece la respuesta del chiste.
     * @param respuesta Respuesta del chiste
     */
    public void setRespuesta(String respuesta) {
        this.respuesta = respuesta;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Chiste chiste = (Chiste) o;
        return id == chiste.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    /**
     * Sobrescritura del método toString() para que devuelva el chiste.
     * Lo devuelve empleando un StringBuilder y por medio del método forEach() para recorrer la lista de banderas.
     * @return Chiste como String
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Chiste: ").append(chiste).append(System.lineSeparator());
        sb.append("Respuesta: ").append(respuesta).append(System.lineSeparator());
        sb.append("Categoría: ").append(categoria).append(System.lineSeparator());
        sb.append("Idioma: ").append(lenguaje).append(System.lineSeparator());
        sb.append("Tipo: ").append(tipo).append(System.lineSeparator());
        sb.append("Banderas: ");
        banderas.forEach(b -> sb.append(b).append(" "));
        sb.append(System.lineSeparator());
        return sb.toString();
    }

}

B) El adapter ChisteDeserializer:

Detalle de implementación de la clase ChisteDeserializer
package com.javhoz.ad.chistes.model;

import com.google.gson.*;

import java.lang.reflect.Type;

/*
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "¿Por qué C consigue todas las chicas y Java no tiene ninguna?",
"delivery": "Porque C no las trata como objetos.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false
},
"safe": true,
"id": 6,
"lang": "es"
}
 */
public class ChisteDeserializer implements JsonDeserializer<Chiste> {

    @Override
    public Chiste deserialize(JsonElement elemento, Type type,
                              JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {

        // Comprobación si es un objeto:
        if (!elemento.isJsonObject())
            return null;

        // Creo un chiste vacío, al que le daré valor a sus atributos:
        Chiste chiste = new Chiste();
        // Recupero el objeto JSON del chiste
        JsonObject jsonChiste = elemento.getAsJsonObject();
        // Comprobación de que no hay error en la petición:
        if (jsonChiste.get("error") != null && jsonChiste.get("error").getAsBoolean()) {
            return null;
        }
        // Compruebo que cada elemento del objeto existe y lo asigno al objeto Chiste:
        // La comprobación se hace con el método get() de la clase JsonObject que devuelve
        // un JsonElement. Si es null, no existe el elemento.
        if (jsonChiste.get("category") != null) {
            chiste.setCategoria(jsonChiste.get("category").getAsString());
        }
        if (jsonChiste.get("type") != null) {
            chiste.setTipo(jsonChiste.get("type").getAsString());
        }
        // En realidad, dependiendo del tipo de chiste, el setup o el delivery pueden no existir.
        // Por lo que podría hacer comprobando el valor de type, pero lo dejo así para que veáis
        // como se puede hacer con el método get() de la clase JsonObject.
        if (jsonChiste.get("setup") != null) {
            chiste.setChiste(jsonChiste.get("setup").getAsString());
            if (jsonChiste.get("delivery") != null) {
                chiste.setRespuesta(jsonChiste.get("delivery").getAsString());
            }
        } else if (jsonChiste.get("joke") != null) {
            chiste.setChiste(jsonChiste.get("joke").getAsString());
        }

        if (jsonChiste.get("lang") != null) {
            chiste.setLenguaje(jsonChiste.get("lang").getAsString());
        }

        if (jsonChiste.get("id") != null) {
            chiste.setId(jsonChiste.get("id").getAsInt());
        }

        if (jsonChiste.get("flags") != null) {
            JsonObject flags = jsonChiste.get("flags").getAsJsonObject();
            if (flags.get("nsfw").getAsBoolean()) {
                chiste.addFlag(Flag.NSFW);
            }
            if (flags.get("religious").getAsBoolean()) {
                chiste.addFlag(Flag.RELIGION);
            }
            if (flags.get("political").getAsBoolean()) {
                chiste.addFlag(Flag.POLITICAL);
            }
            if (flags.get("racist").getAsBoolean()) {
                chiste.addFlag(Flag.RACIST);
            }
            if (flags.get("sexist").getAsBoolean()) {
                chiste.addFlag(Flag.SEXIST);
            }
            if (flags.get("explicit").getAsBoolean()) {
                chiste.addFlag(Flag.EXPLICIT);
            }
        }
        return chiste;
    }
}

C) La clase ChisteTypeAdapter:

Detalle de implementación de la clase ChisteTypeAdapter
package com.javhoz.ad.chistes.model;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;

/*
Formato de JSON:
{
  "id": 1,
  "category": "Programming",
  "type": "single",
  "joke": "Chuck Norris can write multithreaded applications with a single thread.",
  "flags": {
    "nsfw": false,
    "religious": false,
    "political": false,
    "racist": false,
    "sexist": false
  },
  "lang": "en"
 */

/**
 * Updated by javhoz on 16/01/2025.
 * Clase que adaptará el tipo Chiste para que pueda ser serializado y deserializado por Gson.
 *
 * @see com.google.gson.Gson
 * @see com.google.gson.TypeAdapter
 * @see com.google.gson.GsonBuilder
 * @see com.google.gson.JsonDeserializer
 */
public class ChisteTypeAdapter extends TypeAdapter<Chiste> {

    @Override
    public void write(JsonWriter jsonWriter, Chiste chiste) throws IOException {
        jsonWriter.beginObject();
        jsonWriter.name("id").value(chiste.getId());
        jsonWriter.name("category").value(chiste.getCategoriaString());
        jsonWriter.name("type").value(chiste.getTipoString());
        if (chiste.getTipo() == TipoChiste.SINGLE) {
            jsonWriter.name("joke").value(chiste.getChiste());
        } else {
            jsonWriter.name("setup").value(chiste.getChiste());
            jsonWriter.name("delivery").value(chiste.getRespuesta());
        }
        jsonWriter.name("flags");
        jsonWriter.beginObject();
        // Recorremos todas las banderas y asignamos el valor verdadero o falso si el chiste la contiene o no, respectivamente.
        // Puede hacerse por medio del método containsFlag() de la clase Chiste o recoger las banderas
        // del chiste e invocar el método contains() de la clase List.
        for (Flag flag : Flag.values()) {
            jsonWriter.name(flag.getNombre().toLowerCase()).value(chiste.containsFlag(flag));
        }
        jsonWriter.endObject();
        jsonWriter.name("lang").value(chiste.getLenguajeString());
        jsonWriter.endObject();

    }

    /**
     * Método que deserializa un objeto Chiste a partir de un JsonReader.
     *
     * @param reader JsonReader que contiene el objeto Chiste
     * @return Objeto Chiste
     * @throws IOException Si hay un error de E/S
     * @see com.google.gson.stream.JsonReader
     * @see com.google.gson.stream.JsonToken
     */
    @Override
    public Chiste read(JsonReader reader) throws IOException {
        if(reader.peek()== JsonToken.NULL || reader.peek()!= JsonToken.BEGIN_OBJECT){
            // reader.nextNull();
            return null;
        }
        reader.beginObject();
        Chiste chiste = new Chiste();
        while (reader.peek() != JsonToken.END_OBJECT) {
            String name = reader.nextName();
            switch (name) {
                case "id" -> chiste.setId(reader.nextInt());
                case "category" -> chiste.setCategoria(Categoria.getCategoria(reader.nextString()));
                case "type" -> chiste.setTipo(TipoChiste.getTipoChiste(reader.nextString()));
                case "joke", "setup" -> chiste.setChiste(reader.nextString());
                case "delivery" -> chiste.setRespuesta(reader.nextString());
                case "flags" -> // Para hacerlo más modular he puesto el código en un método aparte.
                        readFlags(reader, chiste);
                case "lang" -> chiste.setLenguaje(reader.nextString());
                default -> reader.skipValue();
            }
        }
        reader.endObject();

        return chiste;
    }

    private void readFlags(JsonReader reader, Chiste chiste) throws IOException {
        reader.beginObject();
        while (reader.peek() != JsonToken.END_OBJECT) {
            String flagName = reader.nextName();
            switch (flagName) {
                case "nsfw" -> {
                    if (reader.nextBoolean()) chiste.addFlag(Flag.NSFW);
                }
                case "religious" -> {
                    if (reader.nextBoolean()) chiste.addFlag(Flag.RELIGION);
                }
                case "political" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.POLITICAL);
                }
                case "racist" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.RACIST);
                }
                case "sexist" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.SEXIST);
                }
                case "explicit" -> {
                    if (reader.nextBoolean())
                        chiste.addFlag(Flag.EXPLICIT);
                }
                default -> reader.skipValue();
            }
        }
        reader.endObject();
    }
}

D) La interface IChisteDAO y clase ChisteDAO se usa para obtener los chistes de la API:

Detalle de implementación de la interfaz IChisteDAO
package com.javhoz.ad.chistes.model;

import java.io.Writer;

public interface IChisteDAO {

    String getRandomJokeAsString();
    String getJokeAsString(String categoria, String[] tipo, String[] banderas);
    String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma);

    Chiste getRandomJoke();
    Chiste getJoke(String categoria, String[] tipo, String[] banderas);
    Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma);

    Chiste getJokeById(int id);


    void saveJokeAsJson(Chiste chiste, Writer writer);

}

Podrías realizar mejoras en el código, como la gestión de excepciones, la comprobación de valores nulos, la simplificación de código, etc.

Detalle de implementación de la clase ChisteDAO
package com.javhoz.ad.chistes.model;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Objects;

/**
 * Created by Pepe Calo on 07/11/2023
 * Implementación de la interfaz IChisteDAO que consulta un chiste en un archivo Json
 * mediante la librería Gson.
 * La API de chistes utilizada es:
 * <a href="https://v2.jokeapi.dev/joke/">...</a>
 *
 * @see IChisteDAO
 * @see Chiste
 * @see Gson
 * @see GsonBuilder
 * @see com.google.gson.JsonObject
 * @see com.google.gson.JsonParser
 */
public class ChisteDAO implements IChisteDAO {
    private final Gson gson;

    // https://v2.jokeapi.dev/joke/Programming,Miscellaneous?blacklistFlags=nsfw,religious

    private static final String BASE_URL = "https://v2.jokeapi.dev/joke/";
    private static final String ENDPOINT = "?format=json";
    private static final int NO_ID = 0;

    private static final String SINGLE = "single";

    /**
     * Constructor de la clase ChisteDAO.
     * Si deseas emplear las clases ChisteSerializer y ChisteDeserializer, debes comentar la línea con ChisteTypeAdapter
     * y no comentar las de los otros dos adaptadores.
     */
    public ChisteDAO() {
        gson = new GsonBuilder().setPrettyPrinting()
//                .registerTypeAdapter(Chiste.class, new ChisteDeserializer())
//                .registerTypeAdapter(Chiste.class, new ChisteSerializer())
                .registerTypeAdapter(Chiste.class, new ChisteTypeAdapter())
                .create();
    }


    private String getURL(String categoria, String[] tipo, String[] banderas, String idioma, int id) {
        String url = BASE_URL + categoria + ENDPOINT;
        if (tipo != null && tipo.length > 0) {
            // Concateno los elementos no nulos media stream de un array de String. En el caso de que no haya ninguno, devuelvo un Optional vacío.
            String tipos = Arrays.stream(tipo).filter(Objects::nonNull).reduce((s, s2) -> s + "," + s2).orElse(null);
            if(tipos!=null && !tipos.isEmpty()){
                url += "&type=" + tipos;
            }
        }
        if (banderas != null && banderas.length > 0) {
            String flags = Arrays.stream(banderas).filter(Objects::nonNull).reduce((s, s2) -> s + "," + s2).orElse(null);
            if(flags!=null && !flags.isEmpty()){
                url += "&blacklistFlags=" + flags;
            }
        }
        if (idioma != null && !idioma.isEmpty()) {
            url += "&lang=" + idioma;
        }
        if (id > 0) {
            url += "&idRange=" + id;
        }
        System.out.println("url = " + url);
        return url;
    }

    private Chiste getJoke(String url) {
        try (BufferedReader is = new BufferedReader(new InputStreamReader(new URI(url).toURL().openStream()))) {
            return gson.fromJson(is, Chiste.class);
        } catch (MalformedURLException e) {
            System.err.println("Error en la URL: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Erro E/S: " + e.getMessage());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    private String getJokeAsString(String url) {
        Chiste chiste = getJoke(url);
        return (chiste!=null) ? chiste.getChiste() + System.lineSeparator() + chiste.getRespuesta() : "";
    }


    @Override
    public String getJokeAsString(String categoria, String[] tipo, String[] banderas) {
        return getJokeAsString(getURL(categoria, tipo, banderas, null, NO_ID));
    }


    @Override
    public Chiste getJoke(String categoria, String[] tipo, String[] banderas) {
        return getJoke(getURL(categoria, tipo, banderas, null, NO_ID));
    }

    @Override
    public String getJokeAsString(String categoria, String[] tipo, String[] banderas, String idioma) {
        return getJokeAsString(getURL(categoria, tipo, banderas, idioma, NO_ID));
    }

    @Override
    public Chiste getJoke(String categoria, String[] tipo, String[] banderas, String idioma) {
        return getJoke(getURL(categoria, tipo, banderas, idioma, NO_ID));
    }

    @Override
    public Chiste getJokeById(int id) {
        return getJoke(getURL("Any", null, null, null, id));
    }

    @Override
    public void saveJokeAsJson(Chiste chiste, Writer writer) {
        gson.toJson(chiste, writer);
    }

    @Override
    public String getRandomJokeAsString() {
        System.out.println(BASE_URL + "Any");
        return getJokeAsString(BASE_URL + "Any");
    }

    @Override
    public Chiste getRandomJoke() {
        return getJoke(BASE_URL + "Any");
    }
    
}

Ejercicio

Crear una base de datos con JPA y Hibernate para la aplicación JokeAPI y transfiere todos los datos de JSON a la base de datos.

Añade las dependencias necesarias y el fichero de configuración persistence.xml en el directorio META-INF de src/main/resources:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    <persistence-unit name="chistesH2" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <!--        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>-->
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:h2:RutaABaseDatos;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_UPPER=FALSE;FILE_LOCK=NO"/>
            <property name="jakarta.persistence.jdbc.user" value=""/>
            <property name="jakarta.persistence.jdbc.password" value=""/>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
            <!-- Automáticamente, genera el esquema de la base de datos -->
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>

            <!-- Muestra por pantalla las sentencias SQL -->
            <property name="hibernate.show_sql" value="false"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.highlight_sql" value="true"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        </properties>
    </persistence-unit>
</persistence>

Para ello, crea las siguientes clases:

A) ChisteJpaManager que empleando el patrón Singleton, gestione la creación de la factoría de entidades y el EntityManager.

Solución de ChisteJpaManager
package com.javhoz.ad.chistes.model;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;
import java.util.Map;

import static com.javhoz.ad.chistes.model.ChisteLogger.LOG;

public class ChisteJpaManager {

    public static final String BIBLIOTECA_H2 = "chistesH2";
    public static final String BIBLIOTECA_POSTGRES = "chistesPostgres";


    private static final Map<String, EntityManagerFactory> instancies = new HashMap<>();

    private ChisteJpaManager() {
    }

    private static boolean isEntityManagerFactoryClosed(String unidadPersistencia) {
        return !instancies.containsKey(unidadPersistencia) || instancies.get(unidadPersistencia) == null ||
                !instancies.get(unidadPersistencia).isOpen();
    }

    public static EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        if (isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (ChisteJpaManager.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    try {
                        instancies.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                    } catch (Exception e) {
                        LOG.error("Erro ó crear a unidade de persistencia " + unidadPersistencia +
                                ": " + e.getMessage());
                    }
                }
            }
        }
        return instancies.get(unidadPersistencia);
    }


    public static EntityManager getEntityManager(String persistenceUnitName) {
        return getEntityManagerFactory(persistenceUnitName).createEntityManager();
    }


    public static void close(String persistenceUnitName) {
        if (instancies.containsKey(persistenceUnitName)) {
            instancies.get(persistenceUnitName).close();
            instancies.remove(persistenceUnitName);
        }
    }

}

B) Chiste que emplea JPA para mapear la clase Chiste con la tabla Chiste de la base de datos.

Solución de Chiste
package com.javhoz.ad.chistes.model;

import jakarta.persistence.*;
import java.util.ArrayList;

@Entity
public class Chiste implements java.io.Serializable {
    @Id
    @Column(name = "idChiste")
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    // Como se trata de una relación muchos a muchos, se emplea la anotación @ElementCollection
    // H2 admite el tipo de dato Array de enteros (TINYINT ARRAY), prueba a no poner la anotación @ElementCollection ni @CollectionTable
    @ElementCollection // Para que se cree una tabla intermedia
    @Enumerated(EnumType.STRING)
    @CollectionTable(name = "FlagsChiste", joinColumns = @JoinColumn(name = "idChiste"))
    private final List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
//...
}

C) ChisteDownloader que descarga los chistes de la API y los guarda en la base de datos.

Ten el cuenta que ChisteDownloader es un Singleton y que se puede configurar el número de chistes a descargar, además de un tiempo de espera entre chiste y chiste (la API sólo permite 120 peticiones por minuto).

Por ello, haz que sea un hilo que se ejecute cada cierto tiempo ( implements Runnable ) y tenga los siguientes atributos:

  • tiempoEspera que es el tiempo de espera entre chiste y chiste.
  • instance que es la instancia de ChisteDownloader.
  • MAX_CHISTES que es el número máximo de chistes a descargar.
  • chisteDAO que es el DAO de Chiste.
  • numeroChistes que es el número de chistes a descargar (si no se indica debe ser MAX_CHISTES).
Solución de ChisteDownloader
package com.javhoz.ad.chistes;

import com.javhoz.ad.chistes.model.Chiste;
import com.javhoz.ad.chistes.model.ChisteDAO;
import com.javhoz.ad.chistes.model.ChisteJpaManager;

import static java.lang.Thread.sleep;

public class ChisteDownloader implements Runnable {

    private static final long tiempoEspera = 550;
    private static ChisteDownloader instance;
    private static final int MAX_CHISTES = 200;

    private ChisteDAO chisteDAO;
    private int numeroChistes = MAX_CHISTES;

    private ChisteDownloader() {
        chisteDAO = new ChisteDAO();
    }

    public static ChisteDownloader getInstance() {
        if (instance == null) {
            synchronized (ChisteDownloader.class) {
                if (instance == null) {
                    instance = new ChisteDownloader();
                }
            }
        }
        return instance;
    }


    public void setNumeroChistes(int numeroChistes) {
        this.numeroChistes = numeroChistes;
    }

    @Override
    public void run() {
        chisteDAO = new ChisteDAO();
        var emf = ChisteJpaManager.getEntityManagerFactory(ChisteJpaManager.BIBLIOTECA_H2);
        var em = emf.createEntityManager();

        for (int i = 0; i < numeroChistes; i++) {
            Chiste chiste = chisteDAO.getJokeById(i);
            if (chiste != null) {
                try {
                    em.getTransaction().begin();
                    em.persist(chiste);
                    em.getTransaction().commit();
                } catch (Exception e) {
                    em.getTransaction().rollback();
                }
                System.out.print("*");
                try {
                    sleep(tiempoEspera);
                } catch (InterruptedException e) {
                    System.out.println("Error en el hilo");
                }
            }

        }

    }
}

D) Main que descarga los chistes y los guarda en la base de datos.

Solución de Main
    public static void main(String[] args) {

        ChisteDownloader chisteDownloader = ChisteDownloader.getInstance();
        chisteDownloader.setNumeroChistes(300);
        Thread thread = new Thread(chisteDownloader);
        thread.start();
    }

Ejercicio 05.01. Acceso combinado a la entidad Chiste.

Mofifica la entidad Chiste para que guarde el chiste y la respuesta en un solo campo en la base de datos, pero que se muestren por separado en la aplicación.

@Entity
public class Chiste {
    @Id
    private int id;
    private Categoria categoria;
    private TipoChiste tipo;
    private List<Flag> banderas;
    private String chiste;
    private String respuesta;

    private Lenguaje lenguaje;
    // ...
}

Ejercicio 05.02. CLOB y BLOB de una entidad Documento

Crea una entidad Documento que tenga un campo de texto grande (CLOB) para el contenido del documento y un campo de bytes grande (BLOB) para la imagen del documento. Haz pruebas con tres gestores de bases de datos: H2, SQLite y PostgreSQL y comprueba el resultado creando la tabla en cada uno de ellos, con y sin declaración de tipo de LOB.

    
@Entity
public class Documento {
    @Id
    private long id;
    @Lob
    private String contenido;
    @Lob
    private byte[] imagen;
    // ...
}

Ejercicio 05.03. Conversores personalizados y enumeraciones

Declara una entidad Persona con atributos:

  • idPersona.
  • nombre.
  • apellidos.
  • fechaNacimiento de tipo LocalDate.
  • sexo de tipo enumerado Sexo que puede ser HOMBRE o MUJER.
  • estadoCivil de tipo enumerado EstadoCivil que puede ser SOLTERO, CASADO, DIVORCIADO o VIUDO.
  • foto de tipo byte[].

Realiza las conversiones para que:

  • El nombre y apellidos se guardan en la base de datos como “apellido1, nombre”, con la primera letra de cada palabra en mayúsculas (empleando acceso por campo y por propiedad).
  • La fecha de nacimiento como un entero que representa la edad de la persona en años (obviamente no es la mejor forma de almacenar la edad, pero quiero que practiquéis con los convertidores), usando anotaciones @PostLoad y @PrePersist. Haz pruebas de comportamiento haciendo consultas, inserciones y actualizaciones.
  • Las enumeraciones se guardarán como cadenas en el caso de estado civil y como un carácter de ‘H’ o ‘M’ en el caso del sexo. Hazlo con conversores personalizados.
  • La fotografia se guardará en un campo de tipo BLOB.

Debes completar la entidad Persona y los convertidores necesarios para que funcione correctamente.

public class Persona {
    private long idPersona;
    private String nombre;
    private String apellidos;
    private LocalDate fechaNacimiento;
    private Sexo sexo;
    private EstadoCivil estadoCivil;
    private byte[] foto;
    // ...
}

Hazlo contra la base de datos H2 y comprueba que los datos se guardan correctamente, creando varios registros y recuperándolos.

Ejercicio 05.04. Generación de ids con tabla

A partir del ejecicio anterior con Persona, haz aque el campo idPersona de tipo Long y genera el identificador con una tabla. La tabla debe ser compartida con otras entidades que tengan un campo id de tipo Long.

  • Nombre de la tabla: LONG_ID_GEN
  • Columnas:
    • nomePK.
    • valorPK.
    • El valor de la columna nomePK para la entidad Persona debe ser PERSONA_ID.
    • Dale un valor inicial de 1000 y un tamaño de asignación de 100.

Crea otro generador para esa tabla que se utilizará para la entidad Direccion con un valor inicial de 2000 y un tamaño de asignación de 50.

Haz pruebas de inserción de datos.

Ejercicio 05.05. Generación de ids con una secuencia

Repite el ejercicio anterior con Persona, pero esta vez utiliza una secuencia para generar el identificador en una base de datos H2. Haz pruebas compartiendo la secuencia y sin compartirla. Si puedes, haz lo mismo con una base de datos PostgreSQL.


Ejercicio 05.06. Ampliación de la aplicación de persistencia de una biblioteca

Amplía el ejercicio de la biblioteca para que la entidad Book tenga un identificador generado automáticamente por medio de una tabla.

Además:

  • Crea una enumeración llamada Categoría con los siguientes valores: NOVELA, POESIA, ENSAYO, TEATRO y OTROS.

  • Haz que la entidad Book tenga un atributo de tipo Categoría y que se persista en la base de datos como una cadena. Realiza una conversión de la enumeración a cadena y viceversa de modo que guarde la categoría con el nombre en mayúsculas sólo la primera letra y con acentos.

  • Haz que la columna ISBN sea única, de un tamaño de 13 caracteres y que no pueda ser nula.

  • Crea un atributo de tipo Calendar para la fecha de publicación del libro y haz que se persista en la base de datos como un tipo DATE.

  • Crea un atributo transitorio que sea el número de días que han pasado desde la fecha de publicación hasta la fecha actual. Utiliza la clase java.time.LocalDate para obtener la fecha actual.

  • Crea otro atributo transitorio con el ISBN en versión de 10 dígitos, teniendo en cuenta que el ISBN es un número de 13 dígitos. Para ello, puedes utilizar la clase java.math.BigInteger para realizar la conversión y el siguiente algoritmo:

    1. Elimina los primeros tres dígitos (normalmente 978)
    2. Elimina el último dígito. Ahora tienes nueve dígitos
    3. Ahora necesitas calcular el ‘dígito de control’, que será el décimo dígito de tu ISBN. El objetivo del dígito de control es asegurarse de no haber cometido un error tipográfico: transponer dos dígitos, por ejemplo, o escribir mal uno. Esto es bastante complicado:
    4. Multiplica el primer dígito por 10, el segundo por 9, el tercero por 8 y así sucesivamente, hasta llegar al último dígito (multiplicado por 2).
    5. Ahora tienes una cadena de 9 números nuevos. Agrégalos todos juntos.
    6. Divide esta suma por once. Ahora estás interesado en el resto. Por ejemplo, si la suma fuera 242, que es exactamente 11 x 22, entonces el resto es cero. Si la suma fuera 243, entonces sobraría 1. Tendrás un resto que está entre 0 y 10.
    7. Resta ese resto de 11 para obtener el dígito de control.
    8. Si el resultado es 10, entonces el dígito de control es ‘X’.

Código Java:

    public class ISBN {
        public static void main(String[] args) {
            String isbn = "978-3-16-148410-0";
            String isbn10 = isbn.substring(3, isbn.length() - 1);
            System.out.println(isbn10);
            BigInteger sum = BigInteger.ZERO;
            for (int i = 0; i < isbn10.length(); i++) {
                int digit = Character.getNumericValue(isbn10.charAt(i));
                sum = sum.add(BigInteger.valueOf(digit).multiply(BigInteger.valueOf(10 - i)));
            }
            System.out.println(sum);
            BigInteger remainder = sum.mod(BigInteger.valueOf(11));
            System.out.println(remainder);
            BigInteger controlDigit = BigInteger.valueOf(11).subtract(remainder);
            System.out.println(controlDigit);
            if (controlDigit.intValue() == 10) {
                System.out.println("X");
            } else {
                System.out.println(controlDigit);
            }
        }
    }

Un ejemplo más completo:

   public class ISBNConverter {

    public static void main(String[] args) {
        String isbn13 = "9780123456789"; // ISBN-13
        String isbn10 = convertirISBN13aISBN10(isbn13);
        System.out.println("ISBN-10: " + isbn10);
    }

    public static String convertirISBN13aISBN10(String isbn13) {
        // Verifica si el ISBN-13 proporcionado es válido
        if (!esISBN13Valido(isbn13)) {
            return "ISBN-13 no válido";
        }

        // Elimina los primeros 3 dígitos (978 o 979) del ISBN-13
        String isbn10Parcial = isbn13.substring(3);

        // Calcula el dígito de verificación para el ISBN-10 parcial
        int suma = 0;
        for (int i = 0; i < 9; i++) {
            int digito = Character.getNumericValue(isbn10Parcial.charAt(i));
            suma += (i + 1) * digito;
        }

        int digitoVerificador = suma % 11;
        char digitoVerificadorChar;

        if (digitoVerificador == 10) {
            digitoVerificadorChar = 'X';
        } else {
            digitoVerificadorChar = (char) ('0' + digitoVerificador);
        }

        // Combina el ISBN-10 parcial con el dígito de verificación calculado
        return isbn10Parcial + digitoVerificadorChar;
    }

    public static boolean esISBN13Valido(String isbn13) {
        // Verifica que el ISBN-13 tenga 13 dígitos y comience con "978" o "979"
        return isbn13.matches("^97[89]\\d{10}$");
    }
}

Crea varios libros y pérsistelos en la base de datos (una nueva). Recupéralos y muestra los valores de los datos, incluyendo transitorios.

Ejercicio 06.01. Relación uno a uno bidireccional Equipo-Entrenador

Vamos a crear una aplicación de equipos de la NBA. Cada equipo tiene un entrenador y cada entrenador tiene un equipo, por lo que la relación es uno a uno bidireccional.

Crea las siguientes entidades:

  • Equipo: con los atributos idEquipo, nombre, ciudad, conferencia, division, nombreCompleto y abreviatura.
    • Crea una enumeración Conferencia con los valores ESTE y OESTE.
    • Crea una enumeración Division con los valores ATLANTICO, CENTRAL, SURESTE, NOROESTE, PACIFICO y SUROESTE.
    • En la base de datos, la conferencia y la división se guardarán como cadenas:
      • EAST, WEST
      • ATLANTIC, CENTRAL, SOUTHEAST, NORTHWEST, PACIFIC, SOUTHWEST
    • La abreviatura debe ser única, así como el idEquipo.

Los equipos puedes cargarlos del siguiente archivo JSON:

Ver datos de ejemplo
{
"Equipo": [
	{
		"abreviatura" : "ATL",
		"idEquipo" : 1,
		"ciudad" : "Atlanta",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Hawks",
		"nombreCompleto" : "Atlanta Hawks"
	},
	{
		"abreviatura" : "BOS",
		"idEquipo" : 2,
		"ciudad" : "Boston",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Celtics",
		"nombreCompleto" : "Boston Celtics"
	},
	{
		"abreviatura" : "BKN",
		"idEquipo" : 3,
		"ciudad" : "Brooklyn",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Nets",
		"nombreCompleto" : "Brooklyn Nets"
	},
	{
		"abreviatura" : "CHA",
		"idEquipo" : 4,
		"ciudad" : "Charlotte",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Hornets",
		"nombreCompleto" : "Charlotte Hornets"
	},
	{
		"abreviatura" : "CHI",
		"idEquipo" : 5,
		"ciudad" : "Chicago",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Bulls",
		"nombreCompleto" : "Chicago Bulls"
	},
	{
		"abreviatura" : "CLE",
		"idEquipo" : 6,
		"ciudad" : "Cleveland",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Cavaliers",
		"nombreCompleto" : "Cleveland Cavaliers"
	},
	{
		"abreviatura" : "DAL",
		"idEquipo" : 7,
		"ciudad" : "Dallas",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Mavericks",
		"nombreCompleto" : "Dallas Mavericks"
	},
	{
		"abreviatura" : "DEN",
		"idEquipo" : 8,
		"ciudad" : "Denver",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Nuggets",
		"nombreCompleto" : "Denver Nuggets"
	},
	{
		"abreviatura" : "DET",
		"idEquipo" : 9,
		"ciudad" : "Detroit",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Pistons",
		"nombreCompleto" : "Detroit Pistons"
	},
	{
		"abreviatura" : "GSW",
		"idEquipo" : 10,
		"ciudad" : "Golden State",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Warriors",
		"nombreCompleto" : "Golden State Warriors"
	},
	{
		"abreviatura" : "HOU",
		"idEquipo" : 11,
		"ciudad" : "Houston",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Rockets",
		"nombreCompleto" : "Houston Rockets"
	},
	{
		"abreviatura" : "IND",
		"idEquipo" : 12,
		"ciudad" : "Indiana",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Pacers",
		"nombreCompleto" : "Indiana Pacers"
	},
	{
		"abreviatura" : "LAC",
		"idEquipo" : 13,
		"ciudad" : "LA",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Clippers",
		"nombreCompleto" : "LA Clippers"
	},
	{
		"abreviatura" : "LAL",
		"idEquipo" : 14,
		"ciudad" : "Los Angeles",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Lakers",
		"nombreCompleto" : "Los Angeles Lakers"
	},
	{
		"abreviatura" : "MEM",
		"idEquipo" : 15,
		"ciudad" : "Memphis",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Grizzlies",
		"nombreCompleto" : "Memphis Grizzlies"
	},
	{
		"abreviatura" : "MIA",
		"idEquipo" : 16,
		"ciudad" : "Miami",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Heat",
		"nombreCompleto" : "Miami Heat"
	},
	{
		"abreviatura" : "MIL",
		"idEquipo" : 17,
		"ciudad" : "Milwaukee",
		"conferencia" : "EAST",
		"division" : "CENTRAL",
		"nombre" : "Bucks",
		"nombreCompleto" : "Milwaukee Bucks"
	},
	{
		"abreviatura" : "MIN",
		"idEquipo" : 18,
		"ciudad" : "Minnesota",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Timberwolves",
		"nombreCompleto" : "Minnesota Timberwolves"
	},
	{
		"abreviatura" : "NOP",
		"idEquipo" : 19,
		"ciudad" : "New Orleans",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Pelicans",
		"nombreCompleto" : "New Orleans Pelicans"
	},
	{
		"abreviatura" : "NYK",
		"idEquipo" : 20,
		"ciudad" : "New York",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Knicks",
		"nombreCompleto" : "New York Knicks"
	},
	{
		"abreviatura" : "OKC",
		"idEquipo" : 21,
		"ciudad" : "Oklahoma City",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Thunder",
		"nombreCompleto" : "Oklahoma City Thunder"
	},
	{
		"abreviatura" : "ORL",
		"idEquipo" : 22,
		"ciudad" : "Orlando",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Magic",
		"nombreCompleto" : "Orlando Magic"
	},
	{
		"abreviatura" : "PHI",
		"idEquipo" : 23,
		"ciudad" : "Philadelphia",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "76ers",
		"nombreCompleto" : "Philadelphia 76ers"
	},
	{
		"abreviatura" : "PHX",
		"idEquipo" : 24,
		"ciudad" : "Phoenix",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Suns",
		"nombreCompleto" : "Phoenix Suns"
	},
	{
		"abreviatura" : "POR",
		"idEquipo" : 25,
		"ciudad" : "Portland",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Trail Blazers",
		"nombreCompleto" : "Portland Trail Blazers"
	},
	{
		"abreviatura" : "SAC",
		"idEquipo" : 26,
		"ciudad" : "Sacramento",
		"conferencia" : "WEST",
		"division" : "PACIFIC",
		"nombre" : "Kings",
		"nombreCompleto" : "Sacramento Kings"
	},
	{
		"abreviatura" : "SAS",
		"idEquipo" : 27,
		"ciudad" : "San Antonio",
		"conferencia" : "WEST",
		"division" : "SOUTHWEST",
		"nombre" : "Spurs",
		"nombreCompleto" : "San Antonio Spurs"
	},
	{
		"abreviatura" : "TOR",
		"idEquipo" : 28,
		"ciudad" : "Toronto",
		"conferencia" : "EAST",
		"division" : "ATLANTIC",
		"nombre" : "Raptors",
		"nombreCompleto" : "Toronto Raptors"
	},
	{
		"abreviatura" : "UTA",
		"idEquipo" : 29,
		"ciudad" : "Utah",
		"conferencia" : "WEST",
		"division" : "NORTHWEST",
		"nombre" : "Jazz",
		"nombreCompleto" : "Utah Jazz"
	},
	{
		"abreviatura" : "WAS",
		"idEquipo" : 30,
		"ciudad" : "Washington",
		"conferencia" : "EAST",
		"division" : "SOUTHEAST",
		"nombre" : "Wizards",
		"nombreCompleto" : "Washington Wizards"
	}
]}
  • Entrenador: con los atributos idEntrenador, nombre, fechaNacimiento, salario y equipo.

Mediante JPA e Hibernate, crea una aplicación que permita:

  • Añadir un equipo.
  • Insertar un entrenador.
  • Asignar un entrenador a un equipo.
  • Asignar un equipo a un entrenador.
  • Mostrar los datos de un equipo y su entrenador.

Para ello, debes crear las clases de utilidad necesarias para realizar las operaciones anteriores. JpaNbaManager, EquipoDAO, EntrenadorDAO, etc.

Ejercicio 06.02. Relación muchos a uno unidireccional Jugador-Equipo

Siguiendo el ejemplo anterior, vamos a crear una relación muchos a uno unidireccional entre Jugador y Equipo.

Para ello debe crear una nueva entidad Jugador con los siguientes atributos:

  • idJugador: identificador del jugador.
  • nombre: nombre del jugador.
  • apellidos: apellidos del jugador.
  • equipo: equipo al que pertenece el jugador.
  • altura: altura del jugador (Double).
  • peso: peso del jugador (Double).
  • numero: número de camiseta del jugador (SmallInt).
  • anoDraft: año de elección en el draft (entero).-
  • numeroDraft: número de elección en el draft (SmallInt).
  • rondaDraft: ronda de elección en el draft (SmallInt).
  • posicion: posición en la que juega (base, escolta, alero, ala-pívot, pívot, como enumeración, que debe guardarse como ‘G’, ‘C’, ‘F’, ‘F-C’, ‘C-F’).
  • pais: país de origen del jugador.
  • colegio: universidad o equipo en el que jugó.
  • foto: foto del jugador.

Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una referencia al Equipo y el nombre de la clave foránea sea idEquipo.

Crea jugadores y añádelos a los equipos que has creado en el ejercicio anterior. Completa la aplicación para que puedas añadir jugadores a los equipos y mostrar los jugadores de un equipo.

Datos de ejemplo:

Ver datos de ejemplo
{
"Jugador": [
	{
		"altura" : 198.12,
		"anoDraft" : 2013,
		"idEquipo" : 21,
		"idJugador" : 1,
		"numero" : 8,
		"numeroDraft" : 32,
		"peso" : 86.1825503,
		"posicion" : "G",
		"rondaDraft" : 2,
		"pais" : "Spain",
		"colegio" : "FC Barcelona",
		"nombre" : "Alex",
		"apellido" : "Abrines",
		"foto" : null
	},
	{
		"altura" : 182.88,
		"anoDraft" : null,
		"idEquipo" : 1,
		"idJugador" : 2,
		"numero" : 10,
		"numeroDraft" : null,
		"peso" : 102.05828325,
		"posicion" : "G",
		"rondaDraft" : null,
		"pais" : "USA",
		"colegio" : "St. Bonaventure",
		"nombre" : "Jaylen",
		"apellido" : "Adams",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2013,
		"idEquipo" : 11,
		"idJugador" : 3,
		"numero" : 12,
		"numeroDraft" : 12,
		"peso" : 120.20197805000001,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "New Zealand",
		"colegio" : "Pittsburgh",
		"nombre" : "Steven",
		"apellido" : "Adams",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2017,
		"idEquipo" : 16,
		"idJugador" : 4,
		"numero" : 13,
		"numeroDraft" : 14,
		"peso" : 115.66605435000001,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Kentucky",
		"nombre" : "Bam",
		"apellido" : "Adebayo",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2006,
		"idEquipo" : 3,
		"idJugador" : 6,
		"numero" : 21,
		"numeroDraft" : 2,
		"peso" : 113.3980925,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Texas",
		"nombre" : "LaMarcus",
		"apellido" : "Aldridge",
		"foto" : null
	},
	{
		"altura" : 193.04,
		"anoDraft" : 2018,
		"idEquipo" : 24,
		"idJugador" : 8,
		"numero" : 8,
		"numeroDraft" : 21,
		"peso" : 89.81128926000001,
		"posicion" : "G",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Duke",
		"nombre" : "Grayson",
		"apellido" : "Allen",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2017,
		"idEquipo" : 6,
		"idJugador" : 9,
		"numero" : 31,
		"numeroDraft" : 22,
		"peso" : 110.22294591,
		"posicion" : "C",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Texas",
		"nombre" : "Jarrett",
		"apellido" : "Allen",
		"foto" : null
	},
	{
		"altura" : 203.2,
		"anoDraft" : 2010,
		"idEquipo" : 25,
		"idJugador" : 10,
		"numero" : 5,
		"numeroDraft" : 8,
		"peso" : 99.79032140000001,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Wake Forest",
		"nombre" : "Al-Farouq",
		"apellido" : "Aminu",
		"foto" : null
	},
	{
		"altura" : 195.57999999999998,
		"anoDraft" : 2015,
		"idEquipo" : 12,
		"idJugador" : 11,
		"numero" : 10,
		"numeroDraft" : 21,
		"peso" : 104.77983747,
		"posicion" : "G",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Virginia",
		"nombre" : "Justin",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 205.74,
		"anoDraft" : 2014,
		"idEquipo" : 18,
		"idJugador" : 12,
		"numero" : 1,
		"numeroDraft" : 30,
		"peso" : 104.32624510000001,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "UCLA",
		"nombre" : "Kyle",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 208.28,
		"anoDraft" : 2008,
		"idEquipo" : 19,
		"idJugador" : 13,
		"numero" : 31,
		"numeroDraft" : 21,
		"peso" : 108.8621688,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "California",
		"nombre" : "Ryan",
		"apellido" : "Anderson",
		"foto" : null
	},
	{
		"altura" : 210.82,
		"anoDraft" : 2013,
		"idEquipo" : 17,
		"idJugador" : 15,
		"numero" : 34,
		"numeroDraft" : 15,
		"peso" : 110.22294591,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "Greece",
		"colegio" : "Filathlitikos",
		"nombre" : "Giannis",
		"apellido" : "Antetokounmpo",
		"foto" : null
	},
	{
		"altura" : 208.28,
		"anoDraft" : 2018,
		"idEquipo" : 5,
		"idJugador" : 16,
		"numero" : 37,
		"numeroDraft" : 60,
		"peso" : 90.718474,
		"posicion" : "F",
		"rondaDraft" : 2,
		"pais" : "Greece",
		"colegio" : "Dayton",
		"nombre" : "Kostas",
		"apellido" : "Antetokounmpo",
		"foto" : null
	},
	{
		"altura" : 200.66,
		"anoDraft" : 2003,
		"idEquipo" : 14,
		"idJugador" : 17,
		"numero" : 7,
		"numeroDraft" : 3,
		"peso" : 107.95498406,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "USA",
		"colegio" : "Syracuse",
		"nombre" : "Carmelo",
		"apellido" : "Anthony",
		"foto" : null
	},
	{
		"altura" : 200.66,
		"anoDraft" : 2017,
		"idEquipo" : 20,
		"idJugador" : 18,
		"numero" : 8,
		"numeroDraft" : 23,
		"peso" : 108.8621688,
		"posicion" : "F",
		"rondaDraft" : 1,
		"pais" : "United Kingdom",
		"colegio" : "Indiana",
		"nombre" : "OG",
		"apellido" : "Anunoby",
		"foto" : null
	},
	{
		"altura" : 193.04,
		"anoDraft" : null,
		"idEquipo" : 24,
		"idJugador" : 30053472,
		"numero" : 19,
		"numeroDraft" : null,
		"peso" : 92.07925111,
		"posicion" : null,
		"rondaDraft" : null,
		"pais" : "Denmark",
		"colegio" : "CSKA Moscow",
		"nombre" : "Gabriel",
		"apellido" : "Lundberg",
		"foto" : null
	}
]}

Ejercicio 06.03. Relación muchos a muchos unidireccional Jugador-Posición

Vamos a crear una relación muchos a muchos unidireccional entre Jugador y Posicion. Para eso debes crear una nueva entidad Posicion con los siguientes atributos:

  • idPosicion: identificador de la posición (Long).
  • nombre: nombre de la posición (String, tamaño máximo 50).
  • abreviatura: abreviatura de la posición (String, tamaño máximo 3).
  • descripcion: descripción de la posición (String, tamaño máximo 255).

Haz que la relación sea unidireccional, de modo que la entidad Jugador tenga una colección de Posicion y el nombre de la tabla de unión sea JugadorPosicion.

Crea posiciones y añádelas a los jugadores que has creado en el ejercicio anterior.

Ejercicio 06.04. Mapeo de una base de datos de juegos

Migración de base de datos H2 entre versiones

En la base de datos origen se ejecuta el siguiente script:

SCRIPT TO '<ruta-al-archivo-backup>/backup.sql';

En la base de datos destino se ejecuta el siguiente script:

RUNSCRIPT FROM '<ruta-al-archivo-backup>/backup.sql';
Ejercicio 6.4. Mapeo de una base de datos de juegos.

Disponemos de una base de datos de juegos, que se compone de las siguientes tablas (la base de datos compartida está en el fichero anexo)

Plataforma: idPlataforma, nombre. (Ya contiene datos) Genero: idGenero, nombre. (Ya contiene datos) Juego: idJuego, idGenero (FK), idPlataforma (FK), titulo, miniatura (varchar), estado, descripciónCorta, descripcion, url, editor, desarrollador, fecha. Imagen: idImagen, idJuego (FK), url, imagen (tipo BLOB). RequisitosSistema: idJuego (PK), almacenamiento, graficos, memoria, os, procesador.

Referencias: https://www.freetogame.com/api-doc

  • Las plataformas pueden ser: pc, browser, all, etc. (Ya disponibles en la tabla Plataforma)
  • Las categorías (géneros) pueden ser:
    • mmorpg, shooter, strategy, moba, racing, sports, social, sandbox, open-world, survival, pvp, pve, pixel, voxel, zombie, turn-based, first-person, third-Person, top-down, tank, space, sailing, side-scroller, superhero, permadeath, card, battle-royale, mmo, mmofps, mmotps, 3d, 2d, anime, fantasy, sci-fi, fighting, action-rpg, action, military, martial-arts, flight, low-spec, tower-defense, horror, mmorts, etc. (Ya incorporadas en la tabla Genero)

Cuyos datos se ajustan al formato del siguiente JSON (ejemplo). Debes tener en cuenta que no se ha creado la tabla de requeriminetos mínimos, pero se puede hacer si se desea en una nueva tabla de la base de datos, relacionada, uno a uno:

{
    "id": 452,
    "title": "Call Of Duty: Warzone",
    "thumbnail": "https:\/\/www.freetogame.com\/g\/452\/thumbnail.jpg",
    "status": "Live",
    "short_description": "A standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare.",
    "description": "Call of Duty: Warzone is both a standalone free-to-play battle royale and modes accessible via Call of Duty: Modern Warfare. Warzone features two modes \u2014 the general 150-player battle royle, and \u201cPlunder\u201d. The latter mode is described as a \u201crace to deposit the most Cash\u201d. In both modes players can both earn and loot cash to be used when purchasing in-match equipment, field upgrades, and more. Both cash and XP are earned in a variety of ways, including completing contracts.\r\n\r\nAn interesting feature of the game is one that allows players who have been killed in a match to rejoin it by winning a 1v1 match against other felled players in the Gulag.\r\n\r\nOf course, being a battle royale, the game does offer a battle pass. The pass offers players new weapons, playable characters, Call of Duty points, blueprints, and more. Players can also earn plenty of new items by completing objectives offered with the pass.",
    "game_url": "https:\/\/www.freetogame.com\/open\/call-of-duty-warzone",
    "genre": "Shooter",
    "platform": "Windows",
    "publisher": "Activision",
    "developer": "Infinity Ward",
    "release_date": "2020-03-10",
    "freetogame_profile_url": "https:\/\/www.freetogame.com\/call-of-duty-warzone",
    "minimum_system_requirements": {
        "os": "Windows 7 64-Bit (SP1) or Windows 10 64-Bit",
        "processor": "Intel Core i3-4340 or AMD FX-6300",
        "memory": "8GB RAM",
        "graphics": "NVIDIA GeForce GTX 670 \/ GeForce GTX 1650 or Radeon HD 7950",
        "storage": "175GB HD space"
    },
    "screenshots": [
        {
            "id": 1124,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-1.jpg"
        },
        {
            "id": 1125,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-2.jpg"
        },
        {
            "id": 1126,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-3.jpg"
        },
        {
            "id": 1127,
            "image": "https:\/\/www.freetogame.com\/g\/452\/Call-of-Duty-Warzone-4.jpg"
        }
    ]
}

a) Crea entidades JPA en Java para las tablas de la base de datos, con las siguientes características:

  • Genero: con los atributos idGenero, nombre. La clave es autonumérica.
  • Plataforma: con los atributos idPlataforma y nombre. La clave es autonumérica. Nota: si se hubiese declarado como enumeración, para poder mapear una enumeración en una tabla independiente, obligaría a crear una entidad independiente con el idPlataforma y el nombre. Sin embargo, en este caso, se podría mapear la enumeración directamente en la tabla Juego o declararla como una clase y no como una enumeración.
  • Juego: con todos los atributos de la tabla Juego, incluyendo la relación con Genero y Plataforma. La clave primaria, idJuego, no es autogenerada, es asignada. Ten en cuenta que la relación con la tabla Imagen se trata de una relación uno a muchos, por lo que se deberá declarar una colección de imágenes. Además, el idGenero y el idPlataforma son claves foráneas de las entidades y no deben declararse como atributos de la entidad Juego, sino como objetos del tipo de las entidades Genero y Plataforma.
  • Imagen: con los atributos idImagen (no autogenerada), Juego (relacionada con la entidad Juego @OneToOne), url, imagen (tipo byte[]).
  • RequisitosSistema: relacionada con la tabla Juego. Atributos: idJuego (PK), sistemaOperativo (su nombre no coincide con la columna de la tabla), almacenamiento, graficos, memoria, procesador y su relación juego. Debe emplearse una clave comparta con el idJuego. Para ello debe emplearse la anotación: @MapsId: @MapsId("idJuego").
  @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "idJuego")
  private Juego juego;

b) Haz una sencilla aplicación que cree un juego y lo persista en la base de datos. Ten en cuenta que las claves no son autonuméricas

{"status":0,"status_message":"No game found with that id"}
Bases de datos de videojuegos (H2)

Ejercicio 06.05. Claves compartidas en relaciones uno a uno

Comprueba el funcionamiento de la anotación @PrimaryKeyJoinColumn en una relación uno a uno entre Persona y Departamento. Crea las entidades y realiza pruebas de persistencia.

Persona: idPersona (IDENTITY), nombre, departamento (uno a uno con anotación de @PrimaryKeyJoinColumn) Departamento: idDepartamento (IDENTITY), nombre.

Modifica el ejercicio para que sea bidireccional con @OneToOne y @MapsId en la entidad Departamento y como propietaria de la relación.


Ejercicio 07.01. Elementos embebidos.

Crea una aplicación con JPA para la gestión de películas y series.

  1. Crea una clase InfoContenido con los siguientes atributos:

    • titulo (String): de tamaño 100.
    • genero (String): de tamaño 50.
    • pais (String): de tamaño 2.
    • duracion (int): duración en minutos.
    • año (int): año.
    • sinopsis (String): de tamaño clob.
  2. Crea una entidad Serie con los siguientes atributos:

    • idSerie (long): identificador de la serie. Secuencia.
    • informacion (de tipo InfoContenido)
    • fechaEstreno (LocalDate).
    • temporadas (int): número de temporadas.
    • capitulos (int)
    • directores (lista de String).
  3. Crea una entidad Pelicula con los siguientes atributos:

    • idPelicula (long): identificador de la película. Secuencia.
    • informacion (de tipo InfoContenido)
  • La entidad Serie y Pelicula deben tener el atributo informacion como un objeto embebido.

  • La entidad Pelicula el atributo pais debe ser renombrado a paisPelicula.

  • El atributo directores debe guardarse en una nueva tabla, como una colección con la anotación @ElementCollection (busca información sobre esta anotación).

  • La fecha de estreno, fechaEstreno, de la serie debe guardarse en formato numérico (YYYYMMDD).

Ejercicio 07.02. Clave compuesta en una relación de muchos a muchos.

Data la aplicación de gestión de películas y series, añade dos nuevas entidades: Usuario y Calificacion que permita a los usuarios calificar las películas.

  1. Crea una clase Usuario con los siguientes atributos:

    • idUsuario (long): identificador del usuario. Secuencia.
    • nombre (String): nombre del usuario.
    • email (String): email del usuario.
    • password (String): contraseña del usuario.
    • fechaRegistro (LocalDate): fecha de registro.
  2. Crea una clase Calificacion con los siguientes atributos:

    • calificacion (int): calificación del contenido, con valores de 10 a 0.
    • fechaCalificacion (LocalDate): fecha de la calificación.
    • comentario (String): comentario de la calificación.
    • Además, debe estar relacionado con las entidades Usuario, Pelicula y Serie. Como un usuario puede calificar varias películas y series, y una película o serie puede ser calificada por varios usuarios, es una relación de muchos a muchos. No es preciso que califique series, pues el caso de uso es similar al de las películas.

La clave primaria de la tabla Calificacion debe ser compuesta por los atributos idUsuario, idPelicula.


Ejercicio 07.03. Entidades principales de base de datos de películas.

            <property name="jakarta.persistence.jdbc.url"
                      value="jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas"/>
            <property name="jakarta.persistence.jdbc.user" value="accesoadatos"/>
            <property name="jakarta.persistence.jdbc.password" value="ad123.."/>
            <property name="jakarta.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver"/>
            <property name="jakarta.persistence.schema-generation.database.action" value="none"/>

Sea la siguiente estructura de la base de datos:

Estructura de la base de datos Estructura de la base de datos

SQL de las tablas de la base de datos
CREATE TABLE IF NOT EXISTS `pelicula` (
   `idPelicula` int(10) NOT NULL,
   `musica` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `orixinal` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `ingles` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `castelan` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `xenero` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `anoInicio` smallint(5) DEFAULT NULL,
   `anoFin` smallint(5) DEFAULT NULL,
   `pais` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `duración` smallint(5) DEFAULT NULL,
   `outrasDuracions` varchar(25) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cor` varchar(12) COLLATE utf8_spanish_ci DEFAULT NULL,
   `son` varchar(6) COLLATE utf8_spanish_ci DEFAULT NULL,
   `video` varchar(2) COLLATE utf8_spanish_ci DEFAULT NULL,
   `laserDisc` varchar(2) COLLATE utf8_spanish_ci DEFAULT NULL,
   `texto` longtext COLLATE utf8_spanish_ci,
   `poster` longblob,
   `revisado` varchar(10) COLLATE utf8_spanish_ci DEFAULT NULL,
   PRIMARY KEY (`idPelicula`),
   UNIQUE KEY `Película#PX` (`idPelicula`),
   KEY `Género` (`xenero`),
   KEY `GéneroPelícula` (`xenero`),
   KEY `OriginalAnyo` (`orixinal`,`anoFin`),
   KEY `País` (`pais`),
   KEY `PaísPelícula` (`pais`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

CREATE TABLE IF NOT EXISTS `personaxe` (
   `idPersonaxe` int(10) NOT NULL,
   `importancia` varchar(16) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nome` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nomeOrdenado` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `nomeOrixinal` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `sexo` varchar(6) COLLATE utf8_spanish_ci DEFAULT NULL,
   `dataNacemento` datetime DEFAULT NULL,
   `paisNacemento` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cidadeNacemento` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `dataDefuncion` datetime DEFAULT NULL,
   `paisDefuncion` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `cidadeDefuncion` varchar(125) COLLATE utf8_spanish_ci DEFAULT NULL,
   `estudio` varchar(1) COLLATE utf8_spanish_ci DEFAULT NULL,
   `bio` varchar(1) COLLATE utf8_spanish_ci DEFAULT NULL,
   `texto` longtext COLLATE utf8_spanish_ci,
   `textoFilmografia` longtext COLLATE utf8_spanish_ci,
   `revisado` varchar(10) COLLATE utf8_spanish_ci DEFAULT NULL,
   PRIMARY KEY (`idPersonaxe`),
   UNIQUE KEY `Personaje#PX` (`idPersonaxe`),
   KEY `NomPersona` (`nome`),
   KEY `País de defunción` (`paisDefuncion`),
   KEY `País de nacimiento` (`paisNacemento`),
   KEY `PaísPersonaje` (`paisNacemento`),
   KEY `PaísPersonaje1` (`paisDefuncion`),
   KEY `SexoPersonaxe` (`sexo`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;


CREATE TABLE IF NOT EXISTS `peliculapersonaxe` (
`idPersonaxe` int(10) NOT NULL,
`idPelicula` int(10) NOT NULL,
`ocupacion` varchar(50) COLLATE utf8_spanish_ci NOT NULL,
`personaxeInterpretado` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
PRIMARY KEY (`idPelicula`,`idPersonaxe`,`ocupacion`),
KEY `OcupaciónPelícula_Personaje` (`ocupacion`),
KEY `PersonajePelícula_Personaje` (`idPersonaxe`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

En el que:

  • El título de la película se guarda en el campo castelan.
  • El identificador de la película es entero (no autoincremento).
  • Los participantes de la película están relacionados por medio da de la tabla peliculapersonaxe, en la que el campo ocupacion identifica o tipo de ocupación de la película (‘Actor’, …):
CREATE TABLE IF NOT EXISTS `ocupacion` (
   `ocupacion` varchar(50) COLLATE utf8_spanish_ci DEFAULT NULL,
   `orde` int(11) NOT NULL,
   UNIQUE KEY `Ocupación#PX` (`ocupacion`)
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci;

Ocupación Ocupación

Personaxeocupación Personaxeocupación

Para empezar, crea las entidades Pelicula, Personaxe y Ocupacion. A continuación, crea la entidad PeliculaPersonaxe que relaciona las entidades Pelicula y Personaxe y Ocupacion. Ten en cuenta que tiene un nuevo atributo personaxeInterpretado, que es el nombre del personaje interpretado por el actor en la película.

Mejora:

Crea las entidades asociadas a la base de datos. De modo que las columnas anoInicio, outrasDuracions, video, laserDisc pertenezca a una entidad DetallePelicula de tipo embebido. Hay que tener en cuenta que estos elementos no siempre están presentes, por lo que deben ser opcionales.


Ejercicio 09.01. Ejecución de consultas JPA Películas

Implementa el ejemplo anterior contra una base de datos de películas proporcionada.

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.

Realiza las siguientes consultas:

Las películas que no tienen año de inicio definido:

SELECT p.castelan, p.anoFin, p.anoInicio
FROM Pelicula p
WHERE p.anoInicio IS NOT NULL;

Las películas con una duración superior a 120 minutos:

SELECT p.castelan, p.anoFin, p.duracion
FROM Pelicula p
WHERE p.duracion > 120;

Las películas con Antonio Banderas:

SELECT p FROM Pelicula p JOIN p.personaxes pp JOIN pp.personaxe per WHERE per.nomeOrdenado LIKE 'Antonio Banderas';

Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

Ejercicio 09.02. Creación de consultas JPA Películas

a) Obtener todas las películas que tienen una duración mayor a 120 minutos.

b) Obtener todas las películas que pertenecen a un género específico (por ejemplo, “Drama”).

c) Obtener todas las ocupaciones que tienen más de 5 películas asociadas.

d) Obtener todas las películas que tienen un país específico (por ejemplo, “España”).

e) Obtener todas las películas que tienen al menos un personaje interpretado por un actor de un país específico (por ejemplo, “Francia”).

f) Obtener todas las películas que tienen música compuesta por un compositor específico (por ejemplo, “John Williams”).

g) Obtener todas las películas que tienen un personaje interpretado por un actor con un nombre específico (por ejemplo, “Tom Hanks”).

h) Obtener todas las películas que tienen un género específico y que fueron producidas en un año específico (por ejemplo, “Acción” y 2005).

i) Obtener todas las películas que tienen un personaje interpretado por un actor de un género específico (por ejemplo, “Mujer”).

f) Obtener todas las películas que tienen un personaje interpretado por un actor que nació en un país específico y que tienen una duración mayor a 100 minutos.

g) Devolver todos los países que no tienen películas asociadas, puedes usar una consulta JPQL que utilice una subconsulta o un LEFT JOIN con una condición IS NULL.

Ejercicio 09.03. Consultas SQL a JPQL (películas)

Amplía el ejercicio anterior la base de datos de películas proporcionada.

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se guarden en un archivo de propiedades, persistence.xml, como se ha visto en el tema de configuración.

Las tablas de la base de datos sobre las que hay que implantar las entidades y realizar las consultas son las del ejercicio anterior.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

Realiza las siguientes consultas:

  1. Muestra la película solicitando el id:
SELECT castelan, orixinal, anoFin, poster IS NOT NULL as tenPoster
FROM pelicula WHERE idPelicula = :identificador
  1. Muestra las películas que tienen algún personaje (IS EMPTY) o no tienen personajes (IS NOT EMPTY).

  2. Muestra las películas que tienen personajes con una ocupación concreta:

SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe AND
PP.ocupacion='OCUPACIÓNCONCRETA' AND PP.idPelicula=IDENTIFICADOR_PELICULA
  1. Muestra los títulos de las películas en las que ha trabajado un actor concreto.

  2. Listar el número de películas de acuerdo con el nombre propocionado: (Crea una clase PeliculaDTO con los campos idPelicula, castelan, orixinal, anoFin, tenPoster (booleano) y realiza la consulta)

SELECT idPelicula, castelan, orixinal, anoFin, poster IS NOT NULL as tenPoster
FROM pelicula WHERE castelan LIKE %:nombre% ORDER BY 5 DESC, castelan ASC
  1. Consulta los datos de las ocupaciones de los personajes de una película:
SELECT O.ocupacion FROM ocupacion O WHERE EXISTS (
SELECT idPelicula FROM peliculapersonaxe PP WHERE 
O.ocupacion=PP.ocupacion 
AND PP.idPelicula=IDENTIFICADOR_DE_PELICULA)
AND  O.orde<>0 ORDER BY O.orde

y los nombres sde los personajes que tienen esa ocupación:

SELECT P.nome FROM peliculapersonaxe PP, personaxe P
WHERE P.idPersonaxe=PP.idPersonaxe AND
PP.ocupacion='OCUPACIÓNCONCRETA' AND PP.idPelicula=IDENTIFICADOR_PELICULA

Ejercicio 11.01. Paginación de películas

Ejercicio. Paginación de películas

Características de la base de datos:

URL de la base de datos mysql: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas URL con usuario y contraseña: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Peliculas?user=accesoadatos&password=abc123..&useSSL=false Pese a todo lo mejor es que el usuario y contraseña se referencien en el archivo de propiedades, persistence.xml, de modo independiente, como se ha visto en el tema de configuración.

Las tablas de la base de datos y las entidades ya han sido desarrolladas en apartados anteriores.

Estructura de la base de datos Estructura de la base de datos

Personaxeocupación Personaxeocupación

Se trata de realizar una aplicación que permita consultar las películas por nombre (pide la introducción de un texto) y muestre las películas de la base de datos de 10 en 10. Se debe mostrar el idPelicula, castelan, orixinal, anoFin y el director (relacionado con PeliculaPersonaxe).

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

  • Crea una clase PeliculaPaginaDTO que tenga los campos idPelicula, castelan, orixinal, anoFin y director.
  • Crea una clase PeliculaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.

Ejercicio 12.01. Herencia

Crea una aplicación que permita gestionar una base de datos de vehículos. La base de datos tiene las siguientes tablas:

  • Vehiculo: idVehiculo, marca, modelo, año, precio.
  • Coche: idVehiculo, puertas, plazas.
  • Moto: idVehiculo, cilindrada, tipo.
  • Camion: idVehiculo, carga, ejes.
  • Propietario: idPropietario, nombre, apellidos, vehiculo.

Hazlo con las siguientes estrategias de herencia:

  • MappedSuperclass.
  • Herencia por tabla única.
  • Herencia por tabla por clase.
  • Herencia por tabla por subclase.
  • Herencia por tabla por subclase con discriminador.
  • Herencia por tabla por subclase con discriminador implícito.

Crea una nueva clase Vehiculo que se llame VehiculoDTO que tenga los campos idVehiculo, marca, modelo, año, precio y tipo.

Crea una clase VehiculoDAO que tenga un método que devuelva una lista de vehículos de la base de datos.

Crea las entidades JPA correspondientes y realiza las consultas necesarias para obtener los vehículos de la base de datos.

Hazlo con una base de datos H2 o SQLite.

Última actualización: 23.09.2025

Ejercicios de refuerzo.


Ejercicios JPA

1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe

1.1. EntityManagerFactory Singleton

Crea un EntityManagerFactory con patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:

  • Un método estático, getEmFactory, que devuelva una instancia de EntityManagerFactory, recogiendo el nombre de la unidad de persistencia.
  • Un método estático, getEntityManager, que devuelva una instancia de EntityManager, recogiendo el nombre de la unidad de persistencia.
  • Un método, isEntityManagerFactoryClosed, que devuelva si la factoría es nula o está cerrada.
  • Un método para cerrar la factoría.

1.2. EntityManagerFactory Singleton con propiedades

Añade a la clase anterior un método para que el EntityManagerFactory sea creado con un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:

  • jakarta.persistence.jdbc.url: la URL de la base de datos.
  • jakarta.persistence.jdbc.user: el usuario de la base de datos.
  • jakarta.persistence.jdbc.password: la contraseña de la base de datos.
  • jakarta.persistence.jdbc.driver: el driver de la base de datos.
  • jakarta.persistence.schema-generation.database.action: la acción de la base de datos.
  • jakarta.persistence.schema-generation.create-source: la fuente de creación de la base de datos.

1.3. EntityManagerFactory Singleton para cada unidad de persistencia

Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia. Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.

Realizar un proyecto JPA con EclipseLink que mapee las tablas de la base de datos muestre todos los rangos legales y organismos de la base de datos.

RangoLegal:

idRangoLegal (Integer), nomeG (String), nomeC (String), descripcion (texto largo). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica.

Organismo:

idOrganismo (Integer), nome (String), descripcion (texto largo). El nombre es único. La clave primaria es autonumérica.

  • URL: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Lexislacion
  • DRIVER: org.mariadb.jdbc.Driver
  • USUARIO: lexislacionuser
  • PASSWORD: ABC123..

Crea una base de datos en PostgreSQL con el nombre Lexislacion y las tablas RangoLegal y Organismo.

Haz que la aplicación migre los datos de la base de datos de MariaDB a la de PostgreSQL.

3. Alquiler de películas

Crea una base de datos en PostgreSQL con el nombre videoclub y restaura la base de datos db-videoclub.tar.

Dicha base de datos tiene 15 tablas:

  • actor: almacena datos de actores, incluidos el nombre y el apellido.
  • film: almacena datos de películas como título, año de lanzamiento, duración, clasificación, etc.
  • film_actor: almacena las relaciones entre películas y actores.
  • category: almacena datos de las categorías de las películas.
  • film_category: almacena las relaciones entre películas y categorías.
  • store: contiene los datos de la tienda, incluidos el personal gerencial y la dirección.
  • inventory: almacena datos del inventario.
  • rental: almacena datos de alquiler.
  • payment: almacena los pagos de los clientes.
  • staff: almacena datos del personal.
  • customer: almacena datos de los clientes.
  • address: almacena datos de dirección para el personal y los clientes.
  • city: almacena los nombres de las ciudades.
  • country: almacena los nombres de los países.

Ahora que conocemos todo sobre nuestra base de datos de videoclub de ejemplo, pasemos a cargar la misma base de datos en el servidor de la base de datos PostgreSQL. Los pasos para ello se enumeran a continuación:

Paso 1: Cree una base de datos de videoclub, abriendo la consola SQL. Una vez que abra la consola, deberás añadir las credenciales necesarias para la base de datos, que se verían algo así:

Servidor [localhost]:
Base de datos [postgres]:
Puerto [5432]:
Nombre de usuario [postgres]:
Contraseña para el usuario postgres:

Ahora, usando la declaración CREATE DATABASE, cree una nueva base de datos de la siguiente manera:

CREATE DATABASE videoclub;

Paso 2: Cargue el archivo de la base de datos creando una carpeta en la ubicación deseada (por ejemplo, C:\users\sample_database\bd-videoclub.tar). Ahora abra el símbolo del sistema y navegue hasta la carpeta bin de la carpeta de instalación de PostgreSQL como se muestra a continuación (en el caso de haber añadido la ruta de instalación de PostgreSQL al PATH no será necesario navegar hasta la carpeta bin):

cd C:\ruta\a\la\carpeta\bin

Use la herramienta pg_restore para cargar datos en la base de datos videoclub que acabamos de crear mediante el siguiente comando:

pg_restore -U postgres -d videoclub C:\users\ruta\db-videoclub.tar

Ahora introduce la contraseña de usuario de su base de datos y su base de datos se cargará.

Verificar la carga de la base de datos:

Ahora, si necesitas verificar si la base de datos, usa el siguiente comando para acceder a la base de datos en la consola SQL:

\c

Ahora, para listar todas las tablas en la base de datos, usa el siguiente comando:

\dt
  1. Crea los siguientes tipos de entidad de acuerdo con los estándares Java:
  • Pais que mapee la tabla country: country_id (de tipo serial4), country (varchar(50), last_update (timestamp).
  • Categoria (tabla category): category_id (serial4), name (varchar(25)), last_update (timestamp).
  • Idioma (tabla language): language_id (serial4), name (varchar(20)), last_update (timestamp).
  • Actor (tabla actor): actor_id (serial4), first_name (varchar(45)), last_name (varchar(45)), last_update (timestamp).

Importante: fíjate en los tipos de datos y en las claves primarias, cómo se generan y cómo se relacionan las tablas.

Dichas entidades no contienen relaciones entre sí.

  1. Crea un tipo de entidad, Pelicula, que mapee la tabla film. La creación de la tabla film es la siguiente:
 CREATE TABLE IF NOT EXISTS public.film
 (
 film_id integer NOT NULL DEFAULT nextval('film_film_id_seq'::regclass),
 title character varying(255) COLLATE pg_catalog."default" NOT NULL,
 description text COLLATE pg_catalog."default",
 release_year year,
 language_id smallint NOT NULL,
 rental_duration smallint NOT NULL DEFAULT 3,
 rental_rate numeric(4,2) NOT NULL DEFAULT 4.99,
 length smallint,
 replacement_cost numeric(5,2) NOT NULL DEFAULT 19.99,
 rating mpaa_rating DEFAULT 'G'::mpaa_rating,
 last_update timestamp without time zone NOT NULL DEFAULT now(),
 special_features text[] COLLATE pg_catalog."default",
 fulltext tsvector NOT NULL,
 CONSTRAINT film_pkey PRIMARY KEY (film_id),
 CONSTRAINT "FKbqsvlyhhs40rh7v7e6qpdto5i" FOREIGN KEY (language_id)
 REFERENCES public.language (language_id) MATCH SIMPLE
 ON UPDATE NO ACTION
 ON DELETE NO ACTION,
 CONSTRAINT film_language_id_fkey FOREIGN KEY (language_id)
 REFERENCES public.language (language_id) MATCH SIMPLE
 ON UPDATE CASCADE
 ON DELETE RESTRICT
 )

De momento, mapea el campo fulltext como un String:

  1. Haz que la entidad Pelicula tenga una con la entidad Idioma (un idioma puede tener muchas películas, pero una película sólo puede tener un idioma).

  2. CategoriaPelicula: haz que la entidad Pelicula tenga una relación con la entidad Categoria (una película puede tener muchas categorías y una categoría puede tener muchas películas), para ello, crea una entidad CategoriaPelicula que mapee la tabla film_category, que dispone de las siguientes columnas: film_id (int4), category_id (int4), last_update (timestamp). IMPORTANTE: la clave primaria de la tabla film_category es compuesta por film_id y category_id.

  3. PeliculaActor: haz que la entidad Pelicula tenga una relación con la entidad Actor (una película puede tener muchos actores y un actor pudo haber realizado muchas películas), para ello, crea una entidad PeliculaActor que mapee la tabla film_actor, que dispone de las siguientes columnas: actor_id (int4), film_id (int4), last_update (timestamp). La clave primaria de la tabla film_actor es compuesta por actor_id y film_id.

  4. Ciudad: mapee la tabla city que dispone de las siguientes columnas: city_id (serial4), city (varchar(50)), country_id (int4), last_update (timestamp). Haz que la entidad Ciudad tenga una relación con la entidad Pais (una ciudad pertenece a un único país y un país puede tener muchas ciudades).

  5. Direccion: mapee la tabla address que dispone de las siguientes columnas: address_id (serial4), address (varchar(50)), address2 (varchar(50)), district (varchar(20)), city_id (int4), postal_code (varchar(10)), phone (varchar(20), last_update (timestamp). Haz que la entidad Direccion tenga una relación con la entidad Ciudad (una dirección pertenece a una única ciudad y una ciudad puede tener muchas direcciones).

  6. Empleado: mapee la tabla staff que dispone de las siguientes columnas: staff_id (serial4), first_name (varchar(45)), last_name (varchar(45)), address_id (int4), email (varchar(50)), store_id (int4), active (boolean), username (varchar(16)), password (varchar(40)), last_update (timestamp). Haz que la entidad Empleado tenga una relación con la entidad Direccion (un empleado tiene una dirección y una dirección puede pertenecer a muchos empleados) y con la entidad Tienda.

  7. Tienda: mapee la tabla store que dispone de las siguientes columnas: store_id (serial4), manager_staff_id (int4), address_id (int4), last_update (timestamp). Haz que la entidad Tienda tenga una relación con la entidad Direccion (una tienda tiene una dirección y una dirección puede pertenecer a muchas tiendas).

  8. Inventario: mapee la tabla inventory que dispone de las siguientes columnas: inventory_id (serial4), film_id (int4), store_id (int4), last_update (timestamp). Haz que la entidad Inventario tenga una relación con la entidad Pelicula (un inventario tiene una película y una película puede estar en muchos inventarios) y con la entidad Tienda (un inventario pertenece a una tienda y una tienda puede tener muchos inventarios).

  9. Cliente: mapee la tabla customer que dispone de las siguientes columnas: customer_id (serial4), store_id (int4), first_name (varchar(45)), last_name (varchar(45)), email (varchar(50)), address_id (int4), activebool (boolean), create_date (date), last_update (timestamp), active (int4). Haz que la entidad Cliente tenga una relación con la entidad Tienda (un cliente pertenece a una tienda y una tienda puede tener muchos clientes) y con la entidad Direccion (un cliente tiene una dirección y una dirección puede pertenecer a muchos clientes).

Alquiler: mapee la tabla rental que dispone de las siguientes columnas: rental_id (serial4), rental_date (timestamp), inventory_id (int4), customer_id (int4), return_date (timestamp), staff_id (int4), last_update (timestamp). Haz que la entidad Alquiler tenga una relación con la entidad Inventario (un alquiler tiene un inventario y un inventario puede tener muchos alquileres), con la entidad Cliente (un alquiler tiene un cliente y un cliente puede tener muchos alquileres) y con la entidad Staff (un alquiler tien

  1. Pago: mapee la tabla payment que dispone de las siguientes columnas: payment_id (serial4), customer_id (int4), staff_id (int4), rental_id (int4), amount (numeric(5,2)), payment_date (timestamp). Haz que la entidad Pago tenga una relación con la entidad Alquiler (un pago tiene un alquiler y un alquiler puede tener muchos pagos), con la entidad Cliente (un pago tiene un cliente y un cliente puede tener muchos pagos) y con la entidad Staff (un pago tiene un empleado y un empleado puede tener muchos pagos).

Diagrama de la base de datos:

Diagrama de la base de datos Diagrama de la base de datos

4. Pedidos PostgreSQL

Data la estructura de datos de MariaDB se define en el script bd-pedidos.sql, crea un proyecto JPA con Hibernate que mapee las tablas de la base de datos en PostgreSQL (no crees la base de datos en PostgreSQL, simplemente mapea las tablas, tampoco lo hagas en MariaDB):

CREATE TABLE IF NOT EXISTS public."Producto"
(
    "idProducto" integer NOT NULL DEFAULT nextval('"Producto_idProducto_seq"'::regclass),
    precio double precision,
    nombre character varying(125) COLLATE pg_catalog."default" NOT NULL,
    descripcion character varying(255) COLLATE pg_catalog."default",
    imagen oid,
    CONSTRAINT "Producto_pkey" PRIMARY KEY ("idProducto")
)

CREATE TABLE IF NOT EXISTS public."Cliente"
(
    "idCliente" integer NOT NULL DEFAULT nextval('"Cliente_idCliente_seq"'::regclass),
    dni character varying(12) COLLATE pg_catalog."default" NOT NULL,
    nombre character varying(128) COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT "Cliente_pkey" PRIMARY KEY ("idCliente")
)


CREATE TABLE IF NOT EXISTS public."Pedido"
(
    "idCliente" integer,
    "idPedido" integer NOT NULL DEFAULT nextval('"Pedido_idPedido_seq"'::regclass),
    fecha timestamp(6) without time zone NOT NULL,
    CONSTRAINT "Pedido_pkey" PRIMARY KEY ("idPedido"),
    CONSTRAINT "FKb7xr57df8semvktej7l1lo85e" FOREIGN KEY ("idCliente")
        REFERENCES public."Cliente" ("idCliente") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."Comentario"
(
    "idPedido" integer NOT NULL,
    comentario character varying(255) COLLATE pg_catalog."default",
    CONSTRAINT "FKdne7p3hv47b0l6i5m2efvrpe4" FOREIGN KEY ("idPedido")
        REFERENCES public."Pedido" ("idPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."LineaPedido"
(
    cantidad smallint NOT NULL,
    "idLineaPedido" integer NOT NULL DEFAULT nextval('"LineaPedido_idLineaPedido_seq"'::regclass),
    "idPedido" integer,
    "idProducto" integer,
    CONSTRAINT "LineaPedido_pkey" PRIMARY KEY ("idLineaPedido"),
    CONSTRAINT "FK16r6q9njvef9fuecshutqo5ro" FOREIGN KEY ("idPedido")
        REFERENCES public."Pedido" ("idPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT "FKjmo85q6spgveoxjmyjrvwhk1q" FOREIGN KEY ("idProducto")
        REFERENCES public."Producto" ("idProducto") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."TagLineaPedido"
(
    "idLineaPedido" integer NOT NULL,
    tag character varying(32) COLLATE pg_catalog."default",
    CONSTRAINT "FKfh1px6cx035k4w4615810uxg6" FOREIGN KEY ("idLineaPedido")
        REFERENCES public."LineaPedido" ("idLineaPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

insert into producto(nombre, descripcion, precio, imagen)
values 	('camiseta', 'Camiseta de manga corta.', 15.5, 'img/camiseta.jpg'),
		('pantalon', 'Pantalon vaquero', 30, 'img/pantalon.jpg'),
		('chaqueta', 'Chaqueta de cuero.',  47.75, 'img/chaqueta.jpg'),
		('zapatos', 'Zapatos negros', 100, 'img/zapatos.jpg');

insert into cliente(dni, nombre) 
values 	('11111111A','Daniel'),
		('22222222B','Lucia'),
		('33333333C','Beatriz');

insert into pedido(idCliente, fecha)
values 	(1,'2020-11-05 12:24:37'),
		(2,'2022-10-20 08:34:11');

insert into lineaPedido(idPedido, idProducto, cantidad)
values 	(1, 1, 3),
		(1, 2, 6),
		(2, 2, 10),
		(2, 3, 5),
		(2, 4, 5);

Crea un proyecto con JPA y Hibernate tenga las siguientes entidades:

  • Producto: nombre no nulo. La imagen como bytea.
  • Cliente: dni y nombre no nulo.
  • Pedido: fecha no nula.
  • LineaPedido: cantidad de tipo entero corto y no nula.

Haz que el producto tenga la imagen guardada en la base de datos, no como cadena, de tipo bytea. Los pedidos deben estar ordenados por fecha y las líneas de pedido por cantidad.

  • Producto dispone de una colección de elementos con los comentarios del pedido.
  • LineaPedido debe mapearse como una colección de elementos. Comprueba el resultado y hazlo como entidad.
  • LineaPedido debe tener una colección de tags.

Las relaciones deben actualizarse y borrarse en cascada.

5. Pedidos

Para este ejercicio usaremos la base de datos MariaDB definida en el script bd-pedidos.sql. Deberás crear un proyecto JPA con EclipseLink que mapee las tablas de la base de datos, empleando las entidades del ejercicio anterior, pero mapeadas con EclipseLink.

Crea en el mismo archivo de persistencia, una nueva unidad de persistencia que se conecte a la base de datos de MariaDB con EclipseLink.

Migra los datos de la base de datos de PostgreSQL a la de MariaDB, si es que no lo has hecho en el ejercicio anterior.

La aplicación debe permitir hacer lo siguiente:

  • Mostrar todos los productos de la base de datos.
  • Mostrar todos los pedidos de un cliente.
  • Añadir un pedido.
  • Borrar un pedido.

Para ello, crea una clase AppPedidos con un menú que permita realizar las operaciones anteriores y una clase DAO para cada entidad.

La clase genérica DAO<T, K > recoge el tipo de objeto, el tipo de la clave primaria, que tenga los métodos comunes a todas las entidades. La clase DAO genérica debe tener como atributo un EntityManager. La clase DAO debe tener los métodos necesarios para realizar las operaciones anteriores, así como un atributo de tipo EntityManager.

6. Base de datos de legislación

Propiedades de la conexión a la base de datos
  • URL: jdbc:mariadb://dbalumnos.sanclemente.local:3312/Lexislacion
  • DRIVER: org.mariadb.jdbc.Driver
  • USUARIO: lexislacionuser
  • PASSWORD: ABC123..
Esquema de la base de datos
  • Los nombres de las tablas son en CamelCase, así como los nombres de los atributos. Ten en cuenta que muchos atributos no coinciden con los de las entidades.
  • Las entidades/enumeraciones que debes implantar están en amarillo.
  • Publicacion no es una entidad, es una enumeración, de la aplicación, por lo que no debe ser implantada como entidad (el idPublicacion en Norma coindice con el índice de la enumeración más 1: DOG, BOE, DOCE).

Tabla de la base de datos Tabla de la base de datos

1. JPAUtil

Clase que implanta el patrón Singleton con doble comprobación para obtener el objeto de tipo EntityManager.

public static EntityManagerFactory getEmFactory(String unidadPersistencia)
public static EntityManager getEntityManager()

2. Clases del modelo

  • Las relaciones de la entidad Norma con RangoLegal y Organismo son unidireccionales (RangoLegal, Organimos no tienen referencia en las normas, Publicacion es una enumeración).
RangoLegal

idRangoLegal (Integer), nomeG (String), nomeC (String). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica. Sin relaciones directas.

Organismo

idOrganismo (Integer), nome (String). El nombre es único. La clave primaria es autonumérica. Sin relaciones directas.

Publicacion (enumeración) y PublicacionConverter

Enumeración con 3 valores: DOG, BOE, DOCE, en este orden y atributos llamados idPublicacion, descripcion.

Implanta una clase PublicacionConverter para que el mapeo correcto de los valores de la enumeración en la columna idPublicacion:

public class PublicacionConverter implements AttributeConverter<Publicacion, Integer> {
    
}
Clasificacion

idClasificacion (Integer), nomeG (String), nomeC (String). La clave primaria es autonumérica y los nombres de los atributos no coinciden con los de la base de datos, además, son únicos. Relaciones:

  • Norma, pues hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta, por lo que es una relación muchos a muchos). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Norma

idNorma (Integer), publicacion (Publicacion, enumeración), numeroPublicacion (Integer), numeroPaxina (Integer), titulo (String), dataNorma (LocalDate), dataPublicacion (LocalDate), derogada (boolean). Ten en cuenta que el título es un texto largo (Clob) a la hora de mapear. Relaciones con:

  • Organismo (unidireccional)
  • RangoLegal (unidireccional)
  • DocumentoNorma: el propietario de la relación es DocumentoNorma.
  • Clasificacion hay una tabla intermedia ClasificacionNorma (fíjate que dicha entidad, ClasificacionNorma, no se implanta y que una Norma puede tener muchas clasificaciones y viceversa). La instanciación debe ser “perezosa” con todas las operaciones en cascada.
Documento

idDocumento (Integer), mimeType (String), extension (String), titulo (String), titulo (String), documento (byte[]), tamanho (Integer), idioma (String). Especifica tamaño de los atributos de la tabla. Además, el documento es BLOB y con instanciación perezosa. Relaciones con:

  • DocumentoNorma: el propietario de la relación es DocumentoNorma.
DocumentoNorma

idDocumentoNorma (IdDocumentoNorma), numero (Integer). Ten en cuenta que la clave es compuesta y debes crear una clase IdDocumentoNorma que representa a la clave compuesta. Relaciones con:

  • Norma. Mapea la clave.
  • Documento. Mapea la clave.

3. DTO y Consultas

En la clase AppConsultas realiza las siguientes consultas en JPQL. Ten en cuenta que las consultas JPQL son mucho más sencillas y sólo incorporan la condición de JOIN, el ON, para entidades no relacionadas.

  1. Liste las clasificaciones y la cantidad de normas que contienen (incluidos los que no tienen). Debe devolver en nombre de la clasificación (nombreG), el número de normas (puede ser 0) y el idClasificacion.

Ejemplo de resultado:

ACTIVIDADES CIENTÍFICAS E EDUCATIVAS [30 normas] idClasificacion: 1
ACTIVIDADES INDUSTRIAIS [2 normas] idClasificacion: 2
AGRICULTURA ECOLÓXICA [6 normas] idClasificacion: 3
SELECT C.nome_g, Count(N.idNorma), C.idClasificacion FROM Norma AS N RIGHT JOIN (Clasificacion AS C LEFT JOIN ClasificacionNorma AS CN ON C.idClasificacion = CN.idClasificacion) ON N.idNorma = CN.idNorma GROUP BY C.nome_g, C.idClasificacion ORDER BY 1;
  1. Liste los rangos legales y la cantidad de normas que contienen. Debe devolver el nombre de RangoLegal (nomeG), la cantidad (puede ser 0) y el idRangoLegal.
SELECT R.nome_g, Count(N.idNorma), R.idRangoLegal FROM RangoLegal R LEFT JOIN Norma N ON R.idRangoLegal = N.idRangoLegal GROUP BY R.nome_g, R.idRangoLegal ORDER BY R.idRangoLegal ASC;
  1. Dada la clase NormaDTO del proyecto, realiza una consulta que pida el idRango y muestre las normas, de tipo NormaDTO, con ese idRangoLegal (por ejemplo, idRangoLegal igual a 11). La clase NormaDTO tiene los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.

  2. En la Entidad Norma, crea dos consultas con nombre, llamadas Norma.findByTitulo y Norma.countByTitulo, que devuelvan las normas a partir de un título recogido por parámetro. Haz uso de ellas.

Crea una interface DAO<T, K >, que recoge el tipo de objeto, el tipo de la clave primaria:

import java.util.List;

public interface DAO <T, K>{

    void save(T t);
    void delete(T t);
    T get(K k);
    void update(T t);
    List<T> findAll();
    List<T> findByTituloContaining(String titulo, int offset, int limit);
    List<T> findByTituloContaining(String titulo);
    int countAll();
    int countByTitulo(String titulo);

}
  1. Paginación de Normas

Se trata de realizar una aplicación que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

  • Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
  • Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.

7. NormaDAO y NormaRepository

NormaDAO implements DAO<Norma, Integer>

Implementación mediante patrón DAO de las operaciones con la entidad Norma. Dispone de un atributo privado y final, de tipo EntityManager, em, para referenciar al gestor de entidades, y un constructor que recoge la el objeto de este tipo.

Implantación de los cuatro métodos de la interfaz:

  • T get(K k): devuelve la norma con esa clave.
  • List<T> findByCadenaContaining(String titulo): haciendo uso de la consulta con nombre Norma.findByTitulo consulta las normas que contienen ese título.
  • List<T> findByCadenaContaining(String titulo, int offset, int limit): haciendo uso de la consulta con nombre “Norma.findByTitulo” consulta las normas que contienen ese título y devuelve limit elementos empezando en la posición offset.
  • int countByTitulo(String titulo): haciendo uso de la consulta con nombre Norma.countByTitulo, devuelve el número de normas que contiene ese título.

Comprueba el funcionamiento en la clase AppConsultas.

NormaRepository

Repositorio de String Data JPA, que, además, contiene dos métodos más de los del JpaRepository:

  • Un método que devuelve la lista de Normas que contiene un título (como en el caso anterior).
  • Un método que devuelve el número de normas que contienen el título recogido.

Comprueba el funcionamiento dentro del, creando un método testData dentro de la clase LexislacionApplication.

5. Servicio Rest con Spring Boot Data JPA

Crea una aplicación que accede a datos JPA relacionales a través de una interfaz frontal RESTful basada en Web contra la base de datos de legislación en PostgreSQL. Puedes consultar la documentación de Spring Boot Data JPA en https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference, así como la configuración del archivo properties en https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties.

La aplicación debe permitir realizar las siguientes operaciones:

  • Listar todas las normas.
  • Listar todas las normas que contienen un título.
  • Listar el número de normas que contienen un título.

6. Servicio Rest con Spring Boot Data JPA y paginación

  • Listar todas las normas que contienen un título, de 10 en 10.
  • Listar todas las normas que contienen un título, de 10 en 10, a partir de una página concreta.

Realiza las pruebas con Postman ;-)

Modifica la aplicación para que la paginación se realice con el método findAll de la interfaz PagingAndSortingRepository.

Paginación de Normas: modifica la aplicación para que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

  • Crea una clase NormaDTO que tenga los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.
  • Crea una clase NormaDAO que tenga un método que devuelva el número total de películas y otro que devuelva las lista de películas de una página concreta, ordenadas por año descendente.

Nota: para la realización de una aplicación de consola en Spring Boot, puedes seguir el siguiente tutorial: https://www.baeldung.com/spring-boot-console-app.

Existen varias formas de hacerlo:

  • Usando CommandLineRunner: implementa la interfaz CommandLineRunner y sobreescribe el método run. Ejemplo:
@SpringBootApplication
public class MyApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}
  • Usando ApplicationRunner: implementa la interfaz ApplicationRunner y sobreescribe el método run.

Ejemplo:

@SpringBootApplication
public class MyApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
  • Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}
  • Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

Ejecutores de código al inicio de la Aplicación

Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.

package com.micompanhia.miproyecto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Existen varias formas de hacerlo:

  • Usando CommandLineRunner : implementa la interfaz CommandLineRunner y sobreescribe el método run. Ejemplo:
@SpringBootApplication
public class MiAplicacion implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MiAplicacion implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
  • Usando un @Component: crea una clase anotada con @Component y un método anotado con @PostConstruct:
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}

@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.

@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.

  • Usando un @Bean: crea un método anotado con @Bean en una clase de configuración. El Bean debe devolver un CommandLineRunner o un ApplicationRunner:
@SpringBootApplication
public class MiAplicacion {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.

Diferencias entre ejecutores de código al inicio de la Aplicación

Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:

ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

ApplicationRunner recoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().

CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.

Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.

Puedes encontrar más información con ejemplos en la Guía para Ejecutar Lógica en el Inicio en Spring.

Última actualización: 23.09.2025

Dependencias Maven.

1. Dependencias Maven.

2. Logging

SLF4J (The Simple Logging Facade for Java) es una fachada o interfaz para varios sistemas de registro de eventos (logging) en Java. Permite a los desarrolladores cambiar de sistema de registro de eventos en tiempo de ejecución sin tener que modificar el código fuente. Para más información, visita la página oficial de SLF4J: http://www.slf4j.org/

https://mvnrepository.com/artifact/org.slf4j/slf4j-api https://central.sonatype.com/artifact/org.slf4j/slf4j-api

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.16</version>
</dependency>

Además, precisamos alguna implementación de SLF4J. En este caso vamos a usar Logback:

https://mvnrepository.com/artifact/ch.qos.logback/logback-classic https://central.sonatype.com/artifact/ch.qos.logback/logback-classic

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.5.16</version>
</dependency>

2. Json

2.1 Gson

Gson es una biblioteca Java que se utiliza para convertir objetos Java en su representación JSON. También puede ser utilizado para convertir una cadena JSON en un objeto Java equivalente. Gson es una biblioteca de código abierto desarrollada por Google. Puedes encontrar más información en la página oficial de Gson: https://github.com/google/gson

https://mvnrepository.com/artifact/com.google.code.gson/gson https://central.sonatype.com/artifact/com.google.code.gson/gson

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>

2.2. Jackson Databind

Jackson es una biblioteca Java de código abierto para convertir objetos Java en su representación JSON y viceversa. Jackson es una de las bibliotecas de serialización y deserialización JSON más populares en Java. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-databind

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.2</version>
</dependency>

2.3. Jackson Core

Jackson Core es una biblioteca Java de código abierto para procesar JSON (Stream API). Jackson Core proporciona las clases básicas para trabajar con JSON, como JsonNode, JsonParser y JsonGenerator. Puedes encontrar más información en la página oficial de Jackson: https://github.com/FasterXML/jackson-core

https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core https://central.sonatype.com/artifact/com.fasterxml.jackson.core/jackson-core

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.18.2</version>
</dependency>

3. JUnit

JUnit es un framework open-source que se utiliza para realizar pruebas unitarias en Java. JUnit es una herramienta importante en el desarrollo de software, ya que permite a los desarrolladores probar su código de manera eficiente y asegurarse de que funciona correctamente. Puedes encontrar más información en la página oficial de JUnit: https://junit.org/junit5/

https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api https://central.sonatype.com/artifact/org.junit.jupiter/junit-jupiter-api

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.11.4</version>
    <scope>test</scope>
</dependency>

Ejemplo de uso:

import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;

public class MyTest {

    @Test
    public void test() {
        assertEquals(2, 1 + 1);
    }
}

4. Drivers JDBC

Para trabajar con bases de datos, necesitamos los drivers JDBC correspondientes.

4.1. H2

H2 es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor. Además, admite transacciones, encriptación, funciones de usuario o procedimientos almacenados. Además, puede almacenarse en memoria o en disco.

Puedes encontrar más información en la página oficial de H2: http://www.h2database.com/

https://mvnrepository.com/artifact/com.h2database/h2 https://central.sonatype.com/artifact/com.h2database/h2

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.3.232</version>
</dependency>

Es importante hacer notar que las incompatibilidades entre versiones diferentes de H2, por lo que se recomienda tener control sobre qué versión se está utilizando.

URL: jdbc:h2:mem:testdb (base de datos en memoria) Driver: org.h2.Driver URL (fichero): jdbc:h2:rutaALaBaseDatos;DATABASE_TO_UPPER=false (base de datos en fichero)

El Driver JDBC para H2 hace la conversión automática de los nombres de las tablas y columnas a mayúsculas, por lo que si queremos conservar los nombres originales, debemos añadir DATABASE_TO_UPPER=false a la URL de conexión.

4.2. SQLite JDBC Driver

SQLite es una base de datos relacional embebida, que no requiere un servidor. Es muy ligera y rápida, y se puede utilizar en aplicaciones de escritorio, móviles o en la web. Puedes encontrar más información en la página oficial de SQLite: https://www.sqlite.org/index.html

Existen varias implementaciones de SQLite en Java, pero vamos a usar Xerial SQLite JDBC Driver:

https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc https://central.sonatype.com/artifact/org.xerial/sqlite-jdbc

<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.48.0.0</version>
</dependency>

URL: jdbc:sqlite:rutaALaBaseDatos (base de datos en fichero) Driver: org.sqlite.JDBC

Existen otras API para SQLite, como las versiones originales de androidx: https://developer.android.com/jetpack/androidx/releases/sqlite, pero dicha versión no es compatible con Java SE y se usaba antiguamente para android, antes de la aparicion de Room.

4.3. PostgreSQL JDBC Driver

PostgreSQL es un sistema de gestión de bases de datos relacional de código abierto y muy potente. Puedes encontrar más información en la página oficial de PostgreSQL: https://www.postgresql.org/

https://mvnrepository.com/artifact/org.postgresql/postgresql https://central.sonatype.com/artifact/org.postgresql/postgresql

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.5</version>
</dependency>

URL: jdbc:postgresql://localhost:5432/nombredelabasededatos Driver: org.postgresql.Driver

El usuario y la contraseña se pasarán como parámetros en la URL de conexión:

    String url = "jdbc:postgresql://localhost:5432/nombredelabasededatos";
    String user = "usuario";
    String password = "contraseña";
    Connection conn = DriverManager.getConnection(url, user, password);

Si queremos añadirlos a la URL:

    String url = "jdbc:postgresql://localhost:5432/nombredelabasededatos?user=usuario&password=contraseña";
    Connection conn = DriverManager.getConnection(url);

4.4. MySQL Connector/J

MySQL Connector/J es un controlador JDBC Tipo 4, lo que significa que es una implementación Java pura del protocolo MySQL y no depende de las bibliotecas de cliente MySQL. Como los anteriores, este controlador admite el registro automático con DriverMaganer, lo que significa que no es necesario cargar explícitamente el controlador.

https://mvnrepository.com/artifact/com.mysql/mysql-connector-j https://central.sonatype.com/artifact/com.mysql/mysql-connector-j

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.1.0</version>
</dependency>

URL: jdbc:mysql://localhost:3306/nombredelabasededatos Driver: com.mysql.cj.jdbc.Driver

La URL puede recoger parámetros, como:

    String url = "jdbc:mysql://localhost:3306/nombredelabasededatos?user=usuario&password=contraseña";
    Connection conn = DriverManager.getConnection(url);

4.5. HyperSQL Database (HSQLDB)

HSQLDB es una base de datos relacional escrita en Java. Es muy rápida, de código abierto y se puede ejecutar en modo embebido o en modo servidor: https://hsqldb.org/

https://mvnrepository.com/artifact/org.hsqldb/hsqldb https://central.sonatype.com/artifact/org.hsqldb/hsqldb

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.7.4</version>
</dependency>

URL: jdbc:hsqldb:mem:testdb (base de datos en memoria) Driver: org.hsqldb.jdbc.JDBCDriver URL para servidor: jdbc:hsqldb:hsql://localhost/testdb URL para fichero: jdbc:hsqldb:file:nombrebasededatos

5. Dependencias para JPA

5.1. Jakarta Persistence API (JPA)

La Java Persistence API (JPA) es una especificación de Java que describe la gestión de la persistencia de los objetos en las aplicaciones Java. JPA define un conjunto de interfaces y anotaciones que permiten a los desarrolladores mapear objetos Java a tablas de bases de datos y viceversa. Puedes encontrar más información en la página oficial de JPA:

https://mvnrepository.com/artifact/jakarta.persistence/jakarta.persistence-api https://central.sonatype.com/artifact/jakarta.persistence/jakarta.persistence-api

JPA 3.1:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.1.0</version>
</dependency>

JPA 3.2:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
</dependency>

5.2. Hibernate

Hibernate es un framework de mapeo objeto-relacional (ORM) para Java. Hibernate simplifica el desarrollo de aplicaciones Java que interactúan con bases de datos relacionales. Puedes encontrar más información en la página oficial de Hibernate: https://hibernate.org/

https://mvnrepository.com/artifact/org.hibernate/hibernate-core https://central.sonatype.com/artifact/org.hibernate/hibernate-core

La versión compatible con JPA 3.1 es la versión 6:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.6.5.Final</version>
</dependency>

La versión compatible con JPA 3.2 es la versión 7, que todavía está en desarrollo:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.0.0.Beta3</version>
</dependency>

Pronto se lanzará la versión final de Hibernate 7, que será compatible con JPA 3.2. Esperamos.

EclipseLink es otro framework de mapeo objeto-relacional (ORM) para Java. EclipseLink es una implementación de la especificación JPA y proporciona una serie de características avanzadas, como el mapeo de herencia, el mapeo de tablas, el mapeo de relaciones y la consulta de objetos. Puedes encontrar más información en la página oficial de EclipseLink: https://www.eclipse.org/eclipselink/

https://mvnrepository.com/artifact/org.eclipse.persistence/eclipselink https://central.sonatype.com/artifact/org.eclipse.persistence/eclipselink

La versión compatible con JPA 3.1 es la versión 4:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>4.0.5</version>
</dependency>

La versión compatible con JPA 3.2 es la versión 5, que todavía está en desarrollo:

<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>5.0.0-B05</version>
</dependency>

Pronto se lanzará la versión final de EclipseLink 5, que será compatible con JPA 3.2. Esperamos.

6. Dependencias para Spring

6.1. Spring Core

Spring Core es el núcleo del framework Spring. Proporciona las funcionalidades básicas de Spring, como la inyección de dependencias y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring: https://spring.io/projects/spring-framework

https://mvnrepository.com/artifact/org.springframework/spring-core https://central.sonatype.com/artifact/org.springframework/spring-core

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>6.2.2</version>
</dependency>

6.2. Spring Boot

Spring Boot es un proyecto de Spring que simplifica el desarrollo de aplicaciones Java. Proporciona una serie de características, como la configuración automática, el embebido de servidores, la gestión de dependencias y la creación de aplicaciones ejecutables. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot https://central.sonatype.com/artifact/org.springframework.boot/spring-boot

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot</artifactId>
    <version>3.4.1</version>
</dependency>

Spring Boot Starter es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>3.4.1</version>
</dependency>

6.3. Spring Data JPA

Spring Data JPA es un proyecto de Spring que simplifica el acceso a datos en aplicaciones Java. Proporciona una serie de características, como la creación de repositorios, la generación de consultas y la gestión de transacciones. Puedes encontrar más información en la página oficial de Spring Data JPA: https://spring.io/projects/spring-data-jpa

https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa https://central.sonatype.com/artifact/org.springframework.data/spring-data-jpa

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>3.4.2</version>
</dependency>

6.4. Spring Boot Starter Data JPA

Spring Boot Starter Data JPA es una colección de dependencias que se utilizan comúnmente en las aplicaciones Spring Boot que utilizan Spring Data JPA. Puedes encontrar más información en la página oficial de Spring Boot: https://spring.io/projects/spring-boot

https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.4.1</version>
</dependency>

7. Lenguajes sobre JVM

7.1. Kotlin

Kotlin es un lenguaje de programación moderno y conciso que se ejecuta sobre la JVM. Kotlin es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Kotlin y viceversa. Puedes encontrar más información en la página oficial de Kotlin: https://kotlinlang.org/

IDEs como IntelliJ IDEA o Android Studio soportan Kotlin de forma nativa, pero también puedes usar Kotlin en otros IDE añadiendo las dependencias necesarias.

https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib https://central.sonatype.com/artifact/org.jetbrains.kotlin/kotlin-stdlib

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>2.1.0</version>
</dependency>

7.2. Scala

Scala es un lenguaje de programación funcional y orientado a objetos que se ejecuta sobre la JVM. Scala es interoperable con Java, lo que significa que puedes utilizar las bibliotecas de Java en Scala y viceversa. Puedes encontrar más información en la página oficial de Scala: https://www.scala-lang.org/

IDEs como IntelliJ IDEA o Eclipse soportan Scala de forma nativa, pero también puedes usar Scala en otros IDE añadiendo las dependencias necesarias.

https://mvnrepository.com/artifact/org.scala-lang/scala3-library_3 https://central.sonatype.com/artifact/org.scala-lang/scala3-library_3

<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala3-library_3</artifactId>
    <version>3.6.3</version>
</dependency>

Referencias

Última actualización: 23.09.2025

03.02. Patrón Singleton.


Patrón Singleton

El Singleton es un patrón de diseño creacional que garantiza que una clase tenga solo una instancia, además, proporciona un punto de acceso global a esa instancia.

1. Necesidad del patrón

El patrón Singleton resuelve dos problemas al mismo tiempo, violando el Principio de Responsabilidad Única:

  1. Garantizar que una clase tenga solo una única instancia.
    ¿Por qué alguien querría controlar cuántas instancias tiene una clase? La razón más común es controlar el acceso a un recurso compartido, como una base de datos o un archivo.

    Imagina que creaste un objeto, pero después decides crear uno nuevo. En lugar de recibir un objeto fresco, obtendrás el que ya fue creado anteriormente.

    Este comportamiento es imposible de implementar con un constructor normal, ya que una llamada al constructor siempre debe devolver un objeto nuevo por diseño.

  2. Acceso global a un objeto.
    Los clientes pueden no darse cuenta de que están trabajando con el mismo objeto todo el tiempo.

    Proporciona un punto de acceso global a esa instancia. ¿Recuerdas esas variables globales que solíamos usar para almacenar objetos esenciales? Aunque son convenientes, también son inseguras, ya que cualquier código podría sobrescribirlas y provocar un fallo en la aplicación.

    Al igual que una variable global, el patrón Singleton permite acceder a un objeto desde cualquier parte del programa. Sin embargo, también protege esa instancia para que no sea sobrescrita por otro código.

    Además, no queremos que el código que resuelve el problema esté disperso por todo el programa. Es mejor mantenerlo en una sola clase, especialmente si el resto del código ya depende de ella.

    Hoy en día, el patrón Singleton es tan popular que la gente puede referirse a algo como un Singleton incluso si solo resuelve uno de estos problemas.

2. Implementación

Todas las implementaciones de Singleton tienen en común los siguientes dos pasos:

  1. Hacer que el constructor por defecto sea privado, para evitar que otros objetos usen el operador new con la clase Singleton.
  2. Crear un método de creación estático que actúe como un constructor. Internamente, este método llama al constructor privado para crear un objeto y lo almacena en un campo estático, normalmente llamado instance. Todas las llamadas posteriores a este método devolverán el objeto almacenado en caché.

Si el código tiene acceso a la clase Singleton, entonces puede llamar a su método estático. Así, cada vez que se llame a ese método, siempre se devolverá el mismo objeto.

3. Ejemplos

El gobierno es un excelente ejemplo del patrón Singleton. Un país solo puede tener un único gobierno oficial. Independientemente de la identidad de las personas que lo conforman, el título “Gobierno de Galicia” es un punto de acceso global que identifica al grupo de personas a cargo.

Otro posible ejemplo es una clase de registro. Esta clase controla el acceso a un archivo de registro en el disco. Solo puede haber un archivo de registro en un sistema, y un solo objeto que controla el acceso a ese archivo.

En referencia a acceso a datos, un objeto de conexión a la base de datos también puede ser un Singleton. En este caso, el Singleton controla el acceso a un conjunto de conexiones a la base de datos subyacente. El que creemos una clase Singleton para esto no significa que solo podamos tener una conexión a la base de datos, sino que solo queremos una instancia de la clase que controla las conexiones.

4. Estructura

5. Pseudocódigo

En este ejemplo, la clase de conexión a la base de datos actúa como un Singleton. Esta clase no tiene un constructor público, por lo que la única forma de obtener su objeto es llamando al método getInstance.

// La clase Database define el método `getInstance` que permite
// a los clientes acceder a la misma instancia de una conexión
// a la base de datos en todo el programa.
class Database {
    // Campo estático para almacenar la instancia única.
    private static Database instance;

    // El constructor es privado para evitar llamadas directas con `new`.
    private Database() {
        // Código de inicialización, como la conexión real a la base de datos.
    }

    // Método estático que controla el acceso a la instancia del Singleton.
    public static Database getInstance() {
        if (instance == null) {
            synchronized (Database.class) {
                if (instance == null) {
                    instance = new Database();
                }
            }
        }
        return instance;
    }

    // Método de lógica de negocio para ejecutar consultas.
    public void query(String sql) {
        // Aquí pueden ir restricciones o lógica de caché.
    }
}

// Uso del Singleton en la aplicación.
class Application {
    public static void main(String[] args) {
        Database foo = Database.getInstance();
        foo.query("SELECT ...");

        Database bar = Database.getInstance();
        bar.query("SELECT ...");

        // La variable `bar` contiene la misma instancia que `foo`.
    }
}

6. Aplicabilidad

Debería usarr el patrón Singleton cuando:

El patrón Singleton deshabilita todas las demás formas de crear objetos de una clase, excepto mediante un método especial de creación.

Es posible modificar esta restricción para permitir múltiples instancias si es necesario, simplemente cambiando el método getInstance.

7. Cómo Implementarlo

  1. Agrega un campo estático privado (instance) en la clase para almacenar la instancia Singleton.
  2. Declara un método estático público (getInstance) para obtener la instancia Singleton.
  3. Implementa una “inicialización perezosa”, creando el objeto solo en la primera llamada y almacenándolo en el campo estático.
  4. Haz el constructor privado, de modo que solo la clase Singleton pueda llamarlo.
  5. Reemplaza todas las llamadas al constructor en el código cliente por llamadas al método estático de creación.

8. Ventajas y desventajas

Ventajas:

Desventajas:

9. Relación con otros patrones

03.03. Ejercicios de refuerzo (Violín).


Ejercicios JPA

Postgres

Para la mayor parte de estos ejercicios trabajaremos con la base de datos PostgreSQL. La instalación de postgres y configuración inicial puedes consultarla en:

https://manuais.pages.iessanclemente.net/plantillas/dam/ad/02accesobd/02db/0103postgresql/index.html

Además, dispones de referencias en la documentación oficial de PostgreSQL:

https://www.postgresql.org/docs/current/index.html

0. Patrón Singleton

Antes de comenzar con los ejercicios, necesitamos conocer el patrón Singleton y cómo se puede implementar de forma Thread-Safe. Para ello, a modo de ejemplo, empezaremos creando una clase sencilla que implante el patrón Singleton y Thread-Safe, para después aplicarlo a la creación de una clase JPAUtil que disponga de un único EntityManagerFactory para todas las operaciones de persistencia.

Información adicional sobre el patrón Singleton:

https://manuais.pages.iessanclemente.net/plantillas/dam/ad/00ayudas/04patronesdisenho/02singleton/index.html

Singleton NumeroGenerado

Crea una clase NumeroGenerado que implemente el patrón Singleton y Thread-Safe. La clase debe disponer de un método generarNumero() que devuelva un número aleatorio entre 0 y 100. Al crear el objeto se generará un número aleatorio que se mantendrá durante toda la ejecución del programa.

Solución Singleton NumeroGenerado
import java.util.Random;

public class NumeroGenerado {
    private static NumeroGenerado instance;
    private int numero;

    private NumeroGenerado() {
        numero = new Random().nextInt(101);
    }

    public static NumeroGenerado getInstance() {
        if (instance == null) {
            synchronized (NumeroGenerado.class) {
                if (instance == null) {
                    instance = new NumeroGenerado();
                }
            }
        }
        return instance;
    }

    public int generarNumero() {
        return numero;
    }
}

Comprueba que el patrón Singleton funciona correctamente con el siguiente programa:

public class Main {
    public static void main(String[] args) {
        NumeroGenerado numeroGenerado = NumeroGenerado.getInstance();
        System.out.println(numeroGenerado.generarNumero());
        System.out.println(numeroGenerado.generarNumero());
        System.out.println(numeroGenerado.generarNumero());
        NumeroGenerado numeroGenerado2 = NumeroGenerado.getInstance();
        System.out.println(numeroGenerado2.generarNumero());
        System.out.println(numeroGenerado2.generarNumero());
        System.out.println(numeroGenerado2.generarNumero());
        NumeroGenerado numeroGenerado3 = NumeroGenerado.getInstance();
        System.out.println(numeroGenerado3.generarNumero());
        System.out.println(numeroGenerado3.generarNumero());
        System.out.println(numeroGenerado3.generarNumero());
    }
}

Haz un programa que cree 10 hilos que generen 2 números aleatorios cada uno y los muestren por pantalla.

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                NumeroGenerado numeroGenerado = NumeroGenerado.getInstance();
                for (int j = 0; j < 2; j++) {
                    System.out.println(numeroGenerado.generarNumero());
                }
            }).start();
        }
    }
}

Singleton Generador de números aleatorios

Crea una clase Generador que implemente el patrón Singleton y Thread-Safe. La clase debe emplear para generar números aleatorios y disponer de un método generarNumero() que devuelva un número aleatorio entre 0 y 100.

Solución Generador
public class Generador {
    private static Generador instance;
    private Random random;

    private Generador() {
        random = new Random();
    }

    public static Generador getInstance() {
        if (instance == null) {
            synchronized (Generador.class) {
                if (instance == null) {
                    instance = new Generador();
                }
            }
        }
        return instance;
    }

    public int generarNumero() {
        return random.nextInt(101);
    }
}

Haz un programa que cree 10 hilos que generen 3 números aleatorios cada uno y los muestren por pantalla.

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Generador generador = Generador.getInstance();
                for (int j = 0; j < 3; j++) {
                    System.out.println(generador.generarNumero());
                }
            }).start();
        }
    }
}

Singleton contenedor de propiedades

Crea una clase Propiedades que implemente el patrón Singleton y Thread-Safe. La clase debe disponer de un método getProperty(String clave) que devuelva el valor de una propiedad almacenada en un Map<String, String>. Al crear el objeto se cargarán las propiedades de un fichero propiedades.properties que se encuentra en el directorio resources del proyecto.

Crea un fichero propiedades.properties en el directorio resources con las siguientes propiedades:

url=jdbc:mariadb://dbalumnos.sanclemente.local:3312/Lexislacion
driver=org.mariadb.jdbc.Driver
usuario=lexislacionuser
password=ABC123..

Comprueba que el patrón Singleton funciona correctamente con el siguiente programa:

public class Main {
    public static void main(String[] args) {
        Propiedades propiedades = Propiedades.getInstance();
        System.out.println(propiedades.getProperty("url"));
        System.out.println(propiedades.getProperty("driver"));
        System.out.println(propiedades.getProperty("usuario"));
        System.out.println(propiedades.getProperty("password"));
        Propiedades p2 = Propiedades.getInstance();
        System.out.println(p2.getProperty("url"));
        System.out.println(p2.getProperty("driver"));
        System.out.println(p2.getProperty("usuario"));
        System.out.println(p2.getProperty("password"));

        if(propiedades == p2){
            System.out.println("Son iguais");
        }
        
    }
}
Properties

Para cargar un fichero de propiedades en un proyecto Java, puedes emplear la clase Properties de Java. Un ejemplo de cómo cargar un fichero de propiedades sería el siguiente:

Properties properties = new Properties();
properties.load(new FileInputStream("propiedades.properties"));

Properties es una clase que hereda de Hashtable<Object, Object> y que se emplea para cargar propiedades de un fichero de texto. Las propiedades se cargan en un objeto de tipo Properties y se acceden a través de su método getProperty(String clave).

Solución Singleton contenedor de propiedades
```java
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class Propiedades {
    public static final String FILE = "src/main/resources/conexion.properties";
    private static Propiedades instance;

    private Map<String, String> propiedades;

    private Propiedades(String file) {
        propiedades = new HashMap<>();
        Properties p = new Properties();
        File f = new File(file);
        try(var br = new BufferedReader(new FileReader((f!=null && f.exists()) ? f : new File(FILE) ))) {
                p.load(br);
                for(var key: p.stringPropertyNames()){
                    propiedades.put(key, p.getProperty(key));
                }
        } catch (FileNotFoundException e) {
            System.out.println("Arquivo non atopado: " + e.getMessage());
        } catch (IOException e) {
            System.out.println("Erro de I/O: " + e.getMessage());
        }
    }

    public static final Propiedades getInstance(String file){
        if(instance == null){
            synchronized (Propiedades.class) {
                if(instance == null) {
                    instance = new Propiedades(file);
                }
            }
        }
        return instance;
    }

    public String getProperty(String key){
        return propiedades.get(key);
    }

}

1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe

1.1. EntityManagerFactory Singleton: JPAUtil

Crea una clase JPAUtil que implemente el patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:

Ten en cuenta que la clase EntityManagerFactory es costosa de crear y debe ser única para toda la aplicación. Por ello, debe ser creada una única vez y reutilizada.

Solución JPAUtil
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class EmfManager {

    private static EmfManager instance;

    private EntityManagerFactory entityManagerFactory;

    private EmfManager(String unidadePersistencia){
        entityManagerFactory = Persistence.createEntityManagerFactory(unidadePersistencia);
    }

    public static EmfManager getInstance(String unidadePersistencia){
        if(instance == null){
            synchronized (EmfManager.class){
                if(instance== null){
                    instance = new EmfManager(unidadePersistencia);
                }
            }
        }
        return instance;
    }

    public EntityManagerFactory getEntityManagerFactory() {
        return entityManagerFactory;
    }

    public boolean isEntityManagerFactoryClosed(){
        return entityManagerFactory == null || !entityManagerFactory.isOpen();
    }

    public EntityManager getEntityManager(){
        if(!isEntityManagerFactoryClosed())
            return entityManagerFactory.createEntityManager();
        return null;
    }
}

1.2. EntityManagerFactory Singleton con propiedades

Añade a la clase anterior un método para que el EntityManagerFactory contenga un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:

1.3. EntityManagerFactory Singleton para cada unidad de persistencia

Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia. Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.

Solución EntityManagerFactoriesUtil
package com.javhoz.ad.suzukiviolin.entities;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;

public class EntityManagerFactoriesUtil {

    private static EntityManagerFactoriesUtil instance;

    private final HashMap<String,EntityManagerFactory> entityManagerFactory;

    private EntityManagerFactoriesUtil(){
        entityManagerFactory = new HashMap<>();
    }

    public static EntityManagerFactoriesUtil getInstance(){
        if(instance == null){
            synchronized (EntityManagerFactoriesUtil.class){
                if(instance== null){
                    instance = new EntityManagerFactoriesUtil();
                }
            }
        }
        return instance;
    }

    /**
     * Método que verifica si la unidad de persistencia está cerrada o no para evitar que se cree más de una vez
     * la misma unidad de persistencia.
     * @param unidadPersistencia
     * @return
     */
    public boolean isEntityManagerFactoryClosed(String unidadPersistencia){
        return !entityManagerFactory.containsKey(unidadPersistencia) || !entityManagerFactory.get(unidadPersistencia).isOpen();
    }

    public EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        // Del mismo modo que hacemos doble chequeo en el patrón Singleton, aquí también lo hacemos
        // para evitar que se cree más de una vez la misma unidad de persistencia. Si bien la clave
        // es el nombre de la unidad de persistencia, no se puede garantizar que no se cree más de una
        // vez la misma unidad de persistencia.
        if(isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (EntityManagerFactoriesUtil.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    entityManagerFactory.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                }
            }
        }
        return entityManagerFactory.get(unidadPersistencia);
    }



    public EntityManager getEntityManager(String unidadPersistencia){
        if(!isEntityManagerFactoryClosed(unidadPersistencia))
            return entityManagerFactory.get(unidadPersistencia).createEntityManager();
        return getEntityManagerFactory(unidadPersistencia).createEntityManager();
    }
}

2. Base de datos SuzukiViolin

Dada la base de datos SuzukiViolin, debes crear una base de datos en PostgreSQL con el nombre SuzukiViolinR en local, creando el archivo de persistencia para crear la base de datos, pues estaremos en modo de creación de esquema.

JPAUtil

Crea una clase JPAUtil Singleton y con doble comprobación que se conecte a la base de datos SuzukiViolinR y que tenga un método getEntityManagerFactory que devuelva un EntityManagerFactory para la unidad de persistencia SuzukiViolinR (resuelto en el ejercicio anterior).

Autor

Crea dos autores y añádelos a la base de datos. Puedes crear los autores que ya existen en la base de datos original.

Haz que la clave primaria de la tabla sea idAutor y se genere de manera automática. Hazlo de los distintos modos que conoces:

Enumeraciones

Las 3 enumeraciones que no debes modificar, pues ya están totalmente implementadas, incluso con métodos de utilidad si fuesen necesarios:

A) Tonalidad o armadura en castellano: SOL_M,… Haz que en la base de datos se guarde como una cadena de texto.

B) Debes realizar las siguientes clases de conversión de tipo, autoaplicable en el caso de género musical (para PlayListType y Tonality, deberás aplicarlas sobre las clases correspondientes):

Relaciones

Crea las siguientes relaciones entre las entidades, tomando las consideraciones de acuerdo con la estructura de la base de datos (a excepción de Author con PiezaMusical, que difiere de la indicada en la base de datos original):



Clase Dao

A) Crea una clase Dao, AuthorDao, que implemente los métodos de acceso a la base de datos. La clase debe tener los siguientes métodos:

Crea un programa que cree un autor y lo guarde en la base de datos. Después, actualiza el autor y lo guarda de nuevo. Por último, elimina el autor de la base de datos.

B) PlayListDao: crea una clase PlayListDao que implemente los métodos de acceso a la base de datos. La clase debe tener los siguientes métodos:

Crea un programa que cree una lista de reproducción y la guarde en la base de datos. Después, actualiza la lista de reproducción y la guarda de nuevo. Por último, elimina la lista de reproducción de la base de datos.

03.04 Ejercicios de refuerzo (II)


Ejercicios JPA

Postgres

Para la mayor parte de estos ejercicios trabajaremos con la base de datos PostgreSQL. La instalación de postgres y configuración inicial puedes consultarla en:

https://manuais.pages.iessanclemente.net/plantillas/dam/ad/02accesobd/02db/0103postgresql/index.html

Además, dispones de referencias en la documentación oficial de PostgreSQL:

https://www.postgresql.org/docs/current/index.html

0. Patrón Singleton

Antes de comenzar con los ejercicios, necesitamos conocer el patrón Singleton y cómo se puede implementar de forma Thread-Safe. Para ello, a modo de ejemplo, empezaremos creando una clase sencilla que implante el patrón Singleton y Thread-Safe, para después aplicarlo a la creación de una clase JPAUtil que disponga de un único EntityManagerFactory para todas las operaciones de persistencia.

Información adicional sobre el patrón Singleton:

https://manuais.pages.iessanclemente.net/plantillas/dam/ad/00ayudas/04patronesdisenho/02singleton/index.html

1. Creación de EntityManagerFactory con patrón Singleton y Thread-Safe

1.1. EntityManagerFactory Singleton: JPAUtil

Crea una clase JPAUtil que implemente el patrón Singleton y Thread-Safe. La clase debe tener las siguientes características:

Ten en cuenta que la clase EntityManagerFactory es costosa de crear y debe ser única para toda la aplicación. Por ello, debe ser creada una única vez y reutilizada.

Solución JPAUtil
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class EmfManager {

    private static EmfManager instance;

    private EntityManagerFactory entityManagerFactory;

    private EmfManager(String unidadePersistencia){
        entityManagerFactory = Persistence.createEntityManagerFactory(unidadePersistencia);
    }

    public static EmfManager getInstance(String unidadePersistencia){
        if(instance == null){
            synchronized (EmfManager.class){
                if(instance== null){
                    instance = new EmfManager(unidadePersistencia);
                }
            }
        }
        return instance;
    }

    public EntityManagerFactory getEntityManagerFactory() {
        return entityManagerFactory;
    }

    public boolean isEntityManagerFactoryClosed(){
        return entityManagerFactory == null || !entityManagerFactory.isOpen();
    }

    public EntityManager getEntityManager(){
        if(!isEntityManagerFactoryClosed())
            return entityManagerFactory.createEntityManager();
        return null;
    }
}

1.2. EntityManagerFactory Singleton con propiedades

Añade a la clase anterior un método para que el EntityManagerFactory contenga un mapa de propiedades que se le pasan al método createEntityManagerFactory() de Persistence. El mapa de propiedades debe tener las siguientes propiedades:

1.3. EntityManagerFactory Singleton para cada unidad de persistencia

Mejora: el EntityManager debe ser creado con el método createEntityManager() de la factoría y debe ser único para cada unidad de persistencia. Para ello, en vez de tener una única instancia de EntityManagerFactory, debes tener un Map de EntityManagerFactory, una para cada unidad de persistencia, en el que la clave sea el nombre de la unidad de persistencia y el valor un objeto de tipo EntityManagerFactory.

Solución EntityManagerFactoriesUtil
package com.javhoz.ad.suzukiviolin.entities;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

import java.util.HashMap;

public class EntityManagerFactoriesUtil {

    private static EntityManagerFactoriesUtil instance;

    private final HashMap<String,EntityManagerFactory> entityManagerFactory;

    private EntityManagerFactoriesUtil(){
        entityManagerFactory = new HashMap<>();
    }

    public static EntityManagerFactoriesUtil getInstance(){
        if(instance == null){
            synchronized (EntityManagerFactoriesUtil.class){
                if(instance== null){
                    instance = new EntityManagerFactoriesUtil();
                }
            }
        }
        return instance;
    }

    /**
     * Método que verifica si la unidad de persistencia está cerrada o no para evitar que se cree más de una vez
     * la misma unidad de persistencia.
     * @param unidadPersistencia
     * @return
     */
    public boolean isEntityManagerFactoryClosed(String unidadPersistencia){
        return !entityManagerFactory.containsKey(unidadPersistencia) || !entityManagerFactory.get(unidadPersistencia).isOpen();
    }

    public EntityManagerFactory getEntityManagerFactory(String unidadPersistencia) {
        // Del mismo modo que hacermos doble chequeo en el patrón Singleton, aquí también lo hacemos
        // para evitar que se cree más de una vez la misma unidad de persistencia. Si bien la clave
        // es el nombre de la unidad de persistencia, no se puede garantizar que no se cree más de una
        // vez la misma unidad de persistencia.
        if(isEntityManagerFactoryClosed(unidadPersistencia)) {
            synchronized (EntityManagerFactoriesUtil.class) {
                if (isEntityManagerFactoryClosed(unidadPersistencia)) {
                    entityManagerFactory.put(unidadPersistencia, Persistence.createEntityManagerFactory(unidadPersistencia));
                }
            }
        }
        return entityManagerFactory.get(unidadPersistencia);
    }



    public EntityManager getEntityManager(String unidadPersistencia){
        if(!isEntityManagerFactoryClosed(unidadPersistencia))
            return entityManagerFactory.get(unidadPersistencia).createEntityManager();
        return getEntityManagerFactory(unidadPersistencia).createEntityManager();
    }
}

Realizar un proyecto JPA con EclipseLink que mapee las tablas de la base de datos muestre todos los rangos legales y organismos de la base de datos.

RangoLegal:

idRangoLegal (Integer), nomeG (String), nomeC (String), descripcion (texto largo). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica.

Organismo:

idOrganismo (Integer), nome (String), descripcion (texto largo). El nombre es único. La clave primaria es autonumérica.

Crea una base de datos en PostgreSQL con el nombre Lexislacion y las tablas RangoLegal y Organismo.

Haz que la aplicación migre los datos de la base de datos de MariaDB a la de PostgreSQL.

3. Alquiler de películas

Crea una base de datos en PostgreSQL con el nombre videoclub y restaura la base de datos db-videoclub.tar.

Dicha base de datos tiene 15 tablas:

Ahora que conocemos todo sobre nuestra base de datos de videoclub de ejemplo, pasemos a cargar la misma base de datos en el servidor de la base de datos PostgreSQL. Los pasos para ello se enumeran a continuación:

Paso 1: Cree una base de datos de videoclub, abriendo la consola SQL. Una vez que abra la consola, deberás añadir las credenciales necesarias para la base de datos, que se verían algo así:

Servidor [localhost]:
Base de datos [postgres]:
Puerto [5432]:
Nombre de usuario [postgres]:
Contraseña para el usuario postgres:

Ahora, usando la declaración CREATE DATABASE, cree una nueva base de datos de la siguiente manera:

CREATE DATABASE videoclub;

Paso 2: Cargue el archivo de la base de datos creando una carpeta en la ubicación deseada (por ejemplo, C:\users\sample_database\bd-videoclub.tar). Ahora abra el símbolo del sistema y navegue hasta la carpeta bin de la carpeta de instalación de PostgreSQL como se muestra a continuación (en el caso de haber añadido la ruta de instalación de PostgreSQL al PATH no será necesario navegar hasta la carpeta bin):

cd C:\ruta\a\la\carpeta\bin

Use la herramienta pg_restore para cargar datos en la base de datos videoclub que acabamos de crear mediante el siguiente comando:

pg_restore -U postgres -d videoclub C:\users\ruta\db-videoclub.tar

Ahora introduce la contraseña de usuario de su base de datos y su base de datos se cargará.

Verificar la carga de la base de datos:

Ahora, si necesitas verificar si la base de datos, usa el siguiente comando para acceder a la base de datos en la consola SQL:

\c

Ahora, para listar todas las tablas en la base de datos, usa el siguiente comando:

\dt
  1. Crea los siguientes tipos de entidad de acuerdo con los estándares Java:

Importante: fíjate en los tipos de datos y en las claves primarias, cómo se generan y cómo se relacionan las tablas.

Dichas entidades no contienen relaciones entre sí.

  1. Crea un tipo de entidad, Pelicula, que mapee la tabla film. La creación de la tabla film es la siguiente:
 CREATE TABLE IF NOT EXISTS public.film
 (
 film_id integer NOT NULL DEFAULT nextval('film_film_id_seq'::regclass),
 title character varying(255) COLLATE pg_catalog."default" NOT NULL,
 description text COLLATE pg_catalog."default",
 release_year year,
 language_id smallint NOT NULL,
 rental_duration smallint NOT NULL DEFAULT 3,
 rental_rate numeric(4,2) NOT NULL DEFAULT 4.99,
 length smallint,
 replacement_cost numeric(5,2) NOT NULL DEFAULT 19.99,
 rating mpaa_rating DEFAULT 'G'::mpaa_rating,
 last_update timestamp without time zone NOT NULL DEFAULT now(),
 special_features text[] COLLATE pg_catalog."default",
 fulltext tsvector NOT NULL,
 CONSTRAINT film_pkey PRIMARY KEY (film_id),
 CONSTRAINT "FKbqsvlyhhs40rh7v7e6qpdto5i" FOREIGN KEY (language_id)
 REFERENCES public.language (language_id) MATCH SIMPLE
 ON UPDATE NO ACTION
 ON DELETE NO ACTION,
 CONSTRAINT film_language_id_fkey FOREIGN KEY (language_id)
 REFERENCES public.language (language_id) MATCH SIMPLE
 ON UPDATE CASCADE
 ON DELETE RESTRICT
 )

De momento, mapea el campo fulltext como un String:

  1. Haz que la entidad Pelicula tenga una con la entidad Idioma (un idioma puede tener muchas películas, pero una película sólo puede tener un idioma).

  2. CategoriaPelicula: haz que la entidad Pelicula tenga una relación con la entidad Categoria (una película puede tener muchas categorías y una categoría puede tener muchas películas), para ello, crea una entidad CategoriaPelicula que mapee la tabla film_category, que dispone de las siguientes columnas: film_id (int4), category_id (int4), last_update (timestamp). IMPORTANTE: la clave primaria de la tabla film_category es compuesta por film_id y category_id.

  3. PeliculaActor: haz que la entidad Pelicula tenga una relación con la entidad Actor (una película puede tener muchos actores y un actor pudo haber realizado muchas películas), para ello, crea una entidad PeliculaActor que mapee la tabla film_actor, que dispone de las siguientes columnas: actor_id (int4), film_id (int4), last_update (timestamp). La clave primaria de la tabla film_actor es compuesta por actor_id y film_id.

  4. Ciudad: mapee la tabla city que dispone de las siguientes columnas: city_id (serial4), city (varchar(50)), country_id (int4), last_update (timestamp). Haz que la entidad Ciudad tenga una relación con la entidad Pais (una ciudad pertenece a un único país y un país puede tener muchas ciudades).

  5. Direccion: mapee la tabla address que dispone de las siguientes columnas: address_id (serial4), address (varchar(50)), address2 (varchar(50)), district (varchar(20)), city_id (int4), postal_code (varchar(10)), phone (varchar(20), last_update (timestamp). Haz que la entidad Direccion tenga una relación con la entidad Ciudad (una dirección pertenece a una única ciudad y una ciudad puede tener muchas direcciones).

  6. Empleado: mapee la tabla staff que dispone de las siguientes columnas: staff_id (serial4), first_name (varchar(45)), last_name (varchar(45)), address_id (int4), email (varchar(50)), store_id (int4), active (boolean), username (varchar(16)), password (varchar(40)), last_update (timestamp). Haz que la entidad Empleado tenga una relación con la entidad Direccion (un empleado tiene una dirección y una dirección puede pertenecer a muchos empleados) y con la entidad Tienda.

  7. Tienda: mapee la tabla store que dispone de las siguientes columnas: store_id (serial4), manager_staff_id (int4), address_id (int4), last_update (timestamp). Haz que la entidad Tienda tenga una relación con la entidad Direccion (una tienda tiene una dirección y una dirección puede pertenecer a muchas tiendas).

  8. Inventario: mapee la tabla inventory que dispone de las siguientes columnas: inventory_id (serial4), film_id (int4), store_id (int4), last_update (timestamp). Haz que la entidad Inventario tenga una relación con la entidad Pelicula (un inventario tiene una película y una película puede estar en muchos inventarios) y con la entidad Tienda (un inventario pertenece a una tienda y una tienda puede tener muchos inventarios).

  9. Cliente: mapee la tabla customer que dispone de las siguientes columnas: customer_id (serial4), store_id (int4), first_name (varchar(45)), last_name (varchar(45)), email (varchar(50)), address_id (int4), activebool (boolean), create_date (date), last_update (timestamp), active (int4). Haz que la entidad Cliente tenga una relación con la entidad Tienda (un cliente pertenece a una tienda y una tienda puede tener muchos clientes) y con la entidad Direccion (un cliente tiene una dirección y una dirección puede pertenecer a muchos clientes).

  10. Alquiler: mapee la tabla rental que dispone de las siguientes columnas: rental_id (serial4), rental_date (timestamp), inventory_id (int4), customer_id (int4), return_date (timestamp), staff_id (int4), last_update (timestamp). Haz que la entidad Alquiler tenga una relación con la entidad Inventario (un alquiler tiene un inventario y un inventario puede tener muchos alquileres), con la entidad Cliente (un alquiler tiene un cliente y un cliente puede tener muchos alquileres) y con la entidad Staff (un alquiler tiene un empleado y un empleado puede tener muchos alquileres).

  11. Pago: mapee la tabla payment que dispone de las siguientes columnas: payment_id (serial4), customer_id (int4), staff_id (int4), rental_id (int4), amount (numeric(5,2)), payment_date (timestamp). Haz que la entidad Pago tenga una relación con la entidad Alquiler (un pago tiene un alquiler y un alquiler puede tener muchos pagos), con la entidad Cliente (un pago tiene un cliente y un cliente puede tener muchos pagos) y con la entidad Staff (un pago tiene un empleado y un empleado puede tener muchos pagos).

Diagrama de la base de datos:

Diagrama de la base de datos Diagrama de la base de datos

4. Pedidos PostgreSQL

Data la estructura de datos de MariaDB se define en el script bd-pedidos.sql, crea un proyecto JPA con Hibernate que mapee las tablas de la base de datos en PostgreSQL (no crees la base de datos en PostgreSQL, simplemente mapea las tablas, tampoco lo hagas en MariaDB):

CREATE TABLE IF NOT EXISTS public."Producto"
(
    "idProducto" integer NOT NULL DEFAULT nextval('"Producto_idProducto_seq"'::regclass),
    precio double precision,
    nombre character varying(125) COLLATE pg_catalog."default" NOT NULL,
    descripcion character varying(255) COLLATE pg_catalog."default",
    imagen oid,
    CONSTRAINT "Producto_pkey" PRIMARY KEY ("idProducto")
)

CREATE TABLE IF NOT EXISTS public."Cliente"
(
    "idCliente" integer NOT NULL DEFAULT nextval('"Cliente_idCliente_seq"'::regclass),
    dni character varying(12) COLLATE pg_catalog."default" NOT NULL,
    nombre character varying(128) COLLATE pg_catalog."default" NOT NULL,
    CONSTRAINT "Cliente_pkey" PRIMARY KEY ("idCliente")
)


CREATE TABLE IF NOT EXISTS public."Pedido"
(
    "idCliente" integer,
    "idPedido" integer NOT NULL DEFAULT nextval('"Pedido_idPedido_seq"'::regclass),
    fecha timestamp(6) without time zone NOT NULL,
    CONSTRAINT "Pedido_pkey" PRIMARY KEY ("idPedido"),
    CONSTRAINT "FKb7xr57df8semvktej7l1lo85e" FOREIGN KEY ("idCliente")
        REFERENCES public."Cliente" ("idCliente") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."Comentario"
(
    "idPedido" integer NOT NULL,
    comentario character varying(255) COLLATE pg_catalog."default",
    CONSTRAINT "FKdne7p3hv47b0l6i5m2efvrpe4" FOREIGN KEY ("idPedido")
        REFERENCES public."Pedido" ("idPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."LineaPedido"
(
    cantidad smallint NOT NULL,
    "idLineaPedido" integer NOT NULL DEFAULT nextval('"LineaPedido_idLineaPedido_seq"'::regclass),
    "idPedido" integer,
    "idProducto" integer,
    CONSTRAINT "LineaPedido_pkey" PRIMARY KEY ("idLineaPedido"),
    CONSTRAINT "FK16r6q9njvef9fuecshutqo5ro" FOREIGN KEY ("idPedido")
        REFERENCES public."Pedido" ("idPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT "FKjmo85q6spgveoxjmyjrvwhk1q" FOREIGN KEY ("idProducto")
        REFERENCES public."Producto" ("idProducto") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

CREATE TABLE IF NOT EXISTS public."TagLineaPedido"
(
    "idLineaPedido" integer NOT NULL,
    tag character varying(32) COLLATE pg_catalog."default",
    CONSTRAINT "FKfh1px6cx035k4w4615810uxg6" FOREIGN KEY ("idLineaPedido")
        REFERENCES public."LineaPedido" ("idLineaPedido") MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION
)

insert into producto(nombre, descripcion, precio, imagen)
values 	('camiseta', 'Camiseta de manga corta.', 15.5, 'img/camiseta.jpg'),
		('pantalon', 'Pantalon vaquero', 30, 'img/pantalon.jpg'),
		('chaqueta', 'Chaqueta de cuero.',  47.75, 'img/chaqueta.jpg'),
		('zapatos', 'Zapatos negros', 100, 'img/zapatos.jpg');

insert into cliente(dni, nombre) 
values 	('11111111A','Daniel'),
		('22222222B','Lucia'),
		('33333333C','Beatriz');

insert into pedido(idCliente, fecha)
values 	(1,'2020-11-05 12:24:37'),
		(2,'2022-10-20 08:34:11');

insert into lineaPedido(idPedido, idProducto, cantidad)
values 	(1, 1, 3),
		(1, 2, 6),
		(2, 2, 10),
		(2, 3, 5),
		(2, 4, 5);

Crea un proyecto con JPA y Hibernate tenga las siguientes entidades:

Haz que el producto tenga la imagen guardada en la base de datos, no como cadena, de tipo bytea. Los pedidos deben estar ordenados por fecha y las líneas de pedido por cantidad.

Las relaciones deben actualizarse y borrarse en cascada.

5. Pedidos

Para este ejercicio usaremos la base de datos MariaDB definida en el script bd-pedidos.sql. Deberás crear un proyecto JPA con EclipseLink que mapee las tablas de la base de datos, empleando las entidades del ejercicio anterior, pero mapeadas con EclipseLink.

Crea en el mismo archivo de persistencia, una nueva unidad de persistencia que se conecte a la base de datos de MariaDB con EclipseLink.

Migra los datos de la base de datos de PostgreSQL a la de MariaDB, si es que no lo has hecho en el ejercicio anterior.

La aplicación debe permitir hacer lo siguiente:

Para ello, crea una clase AppPedidos con un menú que permita realizar las operaciones anteriores y una clase DAO para cada entidad.

La clase genérica DAO<T, K > recoge el tipo de objeto, el tipo de la clave primaria, que tenga los métodos comunes a todas las entidades. La clase DAO genérica debe tener como atributo un EntityManager. La clase DAO debe tener los métodos necesarios para realizar las operaciones anteriores, así como un atributo de tipo EntityManager.

6. Ejercicios Herencia

Ejercicios centrados en la herencia en JPA y las distintas estrategias de mapeo (SINGLE_TABLE, JOINED, TABLE_PER_CLASS) usando PostgreSQL como base de datos.


Nivel Básico: Estrategia SINGLE_TABLE

Objetivo

Aprender a usar la estrategia SINGLE_TABLE y entender cómo JPA gestiona la herencia con una única tabla.

Enunciado
  1. Crea una jerarquía de clases con la clase base Vehiculo:

    • id, marca, modelo
  2. Dos clases hijas:

    • Coche: numPuertas
    • Moto: tipoManillar
  3. Usa la estrategia SINGLE_TABLE en la entidad padre.

  4. Configura persistence.xml para usar PostgreSQL (con hibernate.hbm2ddl.auto=update).

  5. Inserta y consulta objetos de tipo Vehiculo, Coche y Moto.


Nivel Intermedio: Estrategia JOINED

Objetivo

Aplicar la estrategia JOINED para guardar entidades hijas en tablas separadas, mejorando la normalización.

Enunciado
  1. Crea la clase base Empleado:

    • id, nombre, dni
  2. Subclases:

    • Programador: lenguajePrincipal
    • Diseniador: herramientaPreferida
  3. Usa JOINED como estrategia de herencia.

  4. Realiza una consulta que devuelva todos los empleados con su información específica (usa TypedQuery<Empleado>).

  5. Inserta instancias de ambas subclases y observa el resultado en PostgreSQL.


Nivel Avanzado: Estrategia TABLE_PER_CLASS

Objetivo

Experimentar con TABLE_PER_CLASS, ideal cuando las subclases son muy distintas entre sí.

Enunciado
  1. Crea una clase base abstracta Documento:

    • id, titulo, fechaCreacion
  2. Subclases:

    • Factura: importeTotal
    • Informe: responsable
  3. Usa la estrategia TABLE_PER_CLASS.

  4. Añade datos de distintos documentos y consulta desde la clase base.

  5. Observa cómo se crean tablas separadas por subclase y cómo afecta a las consultas con @Inheritance.

Discriminadores

En los ejercicios de SINGLE_TABLE y JOINED, añade:

@DiscriminatorColumn(name = "tipo")

y comprueba cómo afecta a la persistencia y consultas.

03.05 Ejercicios de refuerzo (III)

7. Base de datos de legislación

Propiedades de la conexión a la base de datos

Esquema de la base de datos

Tabla de la base de datos Tabla de la base de datos

1. JPAUtil

Clase que implanta el patrón Singleton con doble comprobación para obtener el objeto de tipo EntityManager.

public static EntityManagerFactory getEmFactory(String unidadPersistencia)
public static EntityManager getEntityManager()

2. Clases del modelo

RangoLegal

idRangoLegal (Integer), nomeG (String), nomeC (String). Los nombres de los atributos nomeG y nomeG no coinciden con los de la base de datos y tienen tamaño 128, además, son únicos. La clave primaria es auto numérica. Sin relaciones directas.

Organismo

idOrganismo (Integer), nome (String). El nombre es único. La clave primaria es autonumérica. Sin relaciones directas.

Publicacion (enumeración) y PublicacionConverter

Enumeración con 3 valores: DOG, BOE, DOCE, en este orden y atributos llamados idPublicacion, descripcion.

Implanta una clase PublicacionConverter para que el mapeo correcto de los valores de la enumeración en la columna idPublicacion:

public class PublicacionConverter implements AttributeConverter<Publicacion, Integer> {
    
}

Clasificacion

idClasificacion (Integer), nomeG (String), nomeC (String). La clave primaria es autonumérica y los nombres de los atributos no coinciden con los de la base de datos, además, son únicos. Relaciones:

Norma

idNorma (Integer), publicacion (Publicacion, enumeración), numeroPublicacion (Integer), numeroPaxina (Integer), titulo (String), dataNorma (LocalDate), dataPublicacion (LocalDate), derogada (boolean). Ten en cuenta que el título es un texto largo (Clob) a la hora de mapear. Relaciones con:

Documento

idDocumento (Integer), mimeType (String), extension (String), titulo (String), titulo (String), documento (byte[]), tamanho (Integer), idioma (String). Especifica tamaño de los atributos de la tabla. Además, el documento es BLOB y con instanciación perezosa. Relaciones con:

DocumentoNorma

idDocumentoNorma (IdDocumentoNorma), numero (Integer). Ten en cuenta que la clave es compuesta y debes crear una clase IdDocumentoNorma que representa a la clave compuesta. Relaciones con:

3. DTO y Consultas

En la clase AppConsultas realiza las siguientes consultas en JPQL. Ten en cuenta que las consultas JPQL son mucho más sencillas y sólo incorporan la condición de JOIN, el ON, para entidades no relacionadas.

  1. Liste las clasificaciones y la cantidad de normas que contienen (incluidos los que no tienen). Debe devolver en nombre de la clasificación (nombreG), el número de normas (puede ser 0) y el idClasificacion.

Ejemplo de resultado:

ACTIVIDADES CIENTÍFICAS E EDUCATIVAS [30 normas] idClasificacion: 1
ACTIVIDADES INDUSTRIAIS [2 normas] idClasificacion: 2
AGRICULTURA ECOLÓXICA [6 normas] idClasificacion: 3
SELECT C.nome_g, Count(N.idNorma), C.idClasificacion FROM Norma AS N RIGHT JOIN (Clasificacion AS C LEFT JOIN ClasificacionNorma AS CN ON C.idClasificacion = CN.idClasificacion) ON N.idNorma = CN.idNorma GROUP BY C.nome_g, C.idClasificacion ORDER BY 1;
  1. Liste los rangos legales y la cantidad de normas que contienen. Debe devolver el nombre de RangoLegal (nomeG), la cantidad (puede ser 0) y el idRangoLegal.
SELECT R.nome_g, Count(N.idNorma), R.idRangoLegal FROM RangoLegal R LEFT JOIN Norma N ON R.idRangoLegal = N.idRangoLegal GROUP BY R.nome_g, R.idRangoLegal ORDER BY R.idRangoLegal ASC;
  1. Dada la clase NormaDTO del proyecto, realiza una consulta que pida el idRango y muestre las normas, de tipo NormaDTO, con ese idRangoLegal (por ejemplo, idRangoLegal igual a 11). La clase NormaDTO tiene los campos idNorma, titulo, dataNorma, dataPublicacion, derogada.

  2. En la Entidad Norma, crea dos consultas con nombre, llamadas Norma.findByTitulo y Norma.countByTitulo, que devuelvan las normas a partir de un título recogido por parámetro. Haz uso de ellas.

Crea una interface DAO<T, K >, que recoge el tipo de objeto, el tipo de la clave primaria:

import java.util.List;

public interface DAO <T, K>{

    void save(T t);
    void delete(T t);
    T get(K k);
    void update(T t);
    List<T> findAll();
    List<T> findByTituloContaining(String titulo, int offset, int limit);
    List<T> findByTituloContaining(String titulo);
    int countAll();
    int countByTitulo(String titulo);

}
  1. Paginación de Normas

Se trata de realizar una aplicación que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

4. NormaDAO y NormaRepository

NormaDAO implements DAO<Norma, Integer>

Implementación mediante patrón DAO de las operaciones con la entidad Norma. Dispone de un atributo privado y final, de tipo EntityManager, em, para referenciar al gestor de entidades, y un constructor que recoge la el objeto de este tipo.

Implantación de los cuatro métodos de la interfaz:

Comprueba el funcionamiento en la clase AppConsultas.

NormaRepository

Repositorio de String Data JPA, que, además, contiene dos métodos más de los del JpaRepository:

Comprueba el funcionamiento dentro del, creando un método testData dentro de la clase LexislacionApplication.

5. Servicio Rest con Spring Boot Data JPA

Crea una aplicación que accede a datos JPA relacionales a través de una interfaz frontal RESTful basada en Web contra la base de datos de legislación en PostgreSQL. Puedes consultar la documentación de Spring Boot Data JPA en https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference, así como la configuración del archivo properties en https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties.

La aplicación debe permitir realizar las siguientes operaciones:

6. Servicio Rest con Spring Boot Data JPA y paginación

Realiza las pruebas con Postman ;-)

Modifica la aplicación para que la paginación se realice con el método findAll de la interfaz PagingAndSortingRepository.

Paginación de Normas: modifica la aplicación para que permita consultar las Normas de la base de datos por nombre (pide la introducción de un texto) y muestre las normas de la base de datos de 10 en 10. Se debe mostrar las NormasDTO.

Se debe poder avanzar y retroceder en la paginación. Se debe mostrar el número de página actual y el número total de páginas.

La aplicación debe ser una aplicación de consola, con un menú que permita avanzar y retroceder en la paginación.

Nota: para la realización de una aplicación de consola en Spring Boot, puedes seguir el siguiente tutorial: https://www.baeldung.com/spring-boot-console-app.

Existen varias formas de hacerlo:

@SpringBootApplication
public class MyApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MyApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}
@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

Ejecutores de código al inicio de la Aplicación

Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.

package com.micompanhia.miproyecto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Existen varias formas de hacerlo:

@SpringBootApplication
public class MiAplicacion implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MiAplicacion implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}

@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.

@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.

@SpringBootApplication
public class MiAplicacion {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.

Diferencias entre ejecutores de código al inicio de la Aplicación

Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:

ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

ApplicationRunner recoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().

CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.

Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.

Puedes encontrar más información con ejemplos en la Guía para Ejecutar Lógica en el Inicio en Spring.

UD 4. Spring.

Spring Framework

Introducción a Spring

El Framework Spring es un framework popular y ampliamente utilizado para construir aplicaciones web y empresariales.

A lo largo de los años, el Framework Spring ha crecido exponencialmente al abordar las necesidades de las aplicaciones empresariales modernas, como seguridad, soporte para almacenes de datos NoSQL, gestión de big data, procesamiento por lotes, integración con otros sistemas, y más. Junto con sus proyectos secundarios, Spring se ha convertido en una plataforma viable para construir aplicaciones empresariales.

El Framework Spring es muy flexible y proporciona múltiples formas de configurar los componentes de la aplicación. Con funciones avanzadas combinadas con diversas opciones de configuración, la configuración de aplicaciones Spring se vuelve compleja y propensa a errores. Así, el equipo de Spring creó Spring Boot para abordar la complejidad de la configuración a través de su potente mecanismo de AutoConfiguración.

Descripción general del Framework Spring

El Framework Spring se creó principalmente como un contenedor de inyección de dependencias, pero es mucho más que eso. Spring es famoso por varias razones:

Junto con el Framework Spring, muchos otros subproyectos de Spring ayudan a construir aplicaciones que abordan las necesidades empresariales modernas:

Existen muchos otros proyectos interesantes que abordan diversas necesidades modernas de desarrollo de aplicaciones. Para obtener más información:

https://spring.io/projects.

Subsecciones de UD 4. Spring.

01. Spring Boot.

1. Introducción a Spring Boot

Veremos Spring Boot y sus características y varias opciones para crear una aplicación Spring Boot, como:

Veremos el código generado y cómo ejecutar una aplicación.

2. ¿Qué es Spring Boot?

Spring Boot es un framework que ayuda a los desarrolladores a construir aplicaciones basadas en Spring de manera rápida y fácil.

El objetivo principal de Spring Boot es crear rápidamente aplicaciones basadas en Spring sin necesitar escribir la misma configuración una y otra vez. Las características clave de Spring Boot incluyen:

2.1. Spring Boot starters (Iniciadores de Spring Boot)

Spring Boot ofrece muchos módulos iniciadores (starters) para comenzar rápidamente con muchas de las tecnologías comúnmente utilizadas, como Spring MVC, JPA, Thymeleaf (spring-boot-starter-thymeleaf), MongoDB, Spring Batch, Spring Security, Solr o ElasticSearch…. Estos starters están preconfigurados con las dependencias de bibliotecas más utilizadas, por lo que no es necesario buscar versiones de bibliotecas compatibles y configurarlas manualmente.

Por ejemplo, el módulo starter spring-boot-starter-data-jpa incluye todas las dependencias necesarias para usar Spring Data JPA y las dependencias de la biblioteca Hibernate, ya que Hibernate es la implementación JPA más comúnmente utilizada.

Spring Initializr

Nota: Puedes encontrar una lista de todos los starteres de Spring Boot que vienen por defecto en la documentación oficial en este enlace.

Ejemplo de pom.xml con el starter spring-boot-starter-data-jpa:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. Autoconfiguración de Spring Boot

Spring Boot soluciona el problema de que las aplicaciones de Spring necesitan una configuración compleja al eliminar la necesidad de configurar manualmente la configuración básica.

Spring Boot adopta una vista con opción de la aplicación y configura varios componentes automáticamente registrando beans basados en múltiples criterios. Estos criterios pueden ser:

Por ejemplo, supongamos que tienes la dependencia spring-webmvc en el classpath (o en el archivo pom.xml) y no has registrado explícitamente un bean DispatcherServlet en tu configuración de Spring. En este caso, Spring Boot asume que estás intentando construir una aplicación web basada en SpringMVC y automáticamente intenta registrar DispatcherServlet si aún no está registrado.

Si tienes controladores de base de datos integrados en el classpath, como H2 o HSQL, y no has configurado explícitamente un bean DataSource, Spring Boot registrará automáticamente un bean DataSource utilizando configuraciones de base de datos en memoria.

2.3. Gestión de la Configuración

Spring admite la externalización de propiedades configurables mediante la configuración @PropertySource. Spring Boot lleva esto aún más lejos utilizando valores predeterminados sensatos y una potente vinculación de propiedades de tipo seguro a propiedades de bean. Spring Boot admite tener archivos de configuración separados para perfiles diferentes sin requerir muchas configuraciones.

El archivo application.properties o application.yml es un archivo de configuración que se encuentra en el directorio src/main/resources y contiene propiedades de configuración para la aplicación. Spring Boot carga automáticamente este archivo de configuración y vincula las propiedades a los beans de Spring.

2.4. Spring Boot Actuator

Obtener los diversos detalles de una aplicación en ejecución en producción es crucial para muchas aplicaciones. Spring Boot Actuator proporciona una amplia variedad de características preparadas para producción sin requerir que los desarrolladores escriban mucho código. Algunas de las características del Actuator de Spring son:

2.5. Soporte Fácil de Contenedor de Servlet Integrado

Tradicionalmente, al construir aplicaciones web, se necesitan crear módulos de tipo WAR y luego implementarlos en servidores externos como Tomcat y WildFly. Pero con Spring Boot puedes crear un módulo de tipo JAR e incrustar el contenedor de servlet en la aplicación muy rápidamente para que sea una unidad de implementación independiente. Además, durante el desarrollo, puedes ejecutar rápidamente el módulo de tipo JAR de Spring Boot como una aplicación Java desde el IDE o la línea de comandos utilizando una herramienta de compilación como Maven o Gradle.

3. Primera Aplicación Spring Boot: Spring Initializr y una aplicación web sencilla

Hay muchas formas de crear una aplicación Spring Boot. La forma más sencilla es utilizar Spring Initializr en https://start.spring.io/, un generador de aplicaciones Spring Boot en línea.

En el ejemplo debéis crear una sencilla aplicación web Spring Boot que sirve una página HTML sencilla y ver varios aspectos de una aplicación típica de Spring Boot.

3.1. Uso de Spring Initializr

Accede a http://start.spring.io/ y seguid los pasos a continuación.

  1. Selecciona “Maven Project" y una versión de Spring Boot 3.4.3 (elige la última versión estable).
Nota

Nota: La versión de Spring Boot puede cambiar con el tiempo. Asegúrate de seleccionar la última versión estable. Además, las versiones SNAPSHOT pueden no ser estables y pueden contener errores. Son versiones que no se han lanzado oficialmente.

No hagas caso de la imagen, aparecerá una versión más reciente en el inizializador. Úsala como referencia.

Spring Initializr Spring Initializr

  1. Introduce los detalles del proyecto Maven (ajústalos a las necesidades de tu proyecto):

    • Group (grupo): local.sanclemente.ad
    • Artifact (artefacto): springboot-basic
    • Nombre: springboot-basic
    • Nombre del paquete: local.sanclemente.ad.demo
    • Empaquetado: JAR
    • Versión de Java: 21 (o la versión que prefieras)
    • Lenguaje: Java
  2. Haz clic en el botón "Add Dependencies. Puedes buscar los starters si ya estás familiarizado con sus nombres. Verás muchos módulos organizados en varias categorías, como Core, Web, Template engines, AI, NOSQL o SQL. Selecciona la casilla de verificación “Web” desde la categoría Web. Apacerecerá una lista de dependencias, selecciona “Web”:

Spring Web Web
Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.

Como puedes ver, la dependencia spring-boot-starter-web se selecciona automáticamente. Esta dependencia es necesaria para crear aplicaciones web basadas en Spring MVC e incluye por defecto:

Importante: versiones de dependencias

Spring Initializr selecciona automáticamente las versiones de las dependencias. Si deseas usar una versión específica de una dependencia, puedes especificarla manualmente en el archivo pom.xml.

Además, ten en cuenta que no siempre la última versión de Spring Boot es compatible con todas las dependencias. Si tienes problemas de compatibilidad, puedes probar con una versión anterior de Spring Boot, pues no permitirá seleccionar las dependencias que no sean compatibles.

  1. Haz clic en el botón Generate para descargar el archivo ZIP con el proyecto Spring Boot.

Ahora puedes extraer el archivo ZIP descargado e importarlo en el IDE IntelliJ IDEA, Eclipse o NetBeans.

3.2. Uso de Spring Tool Suite

Spring Tool Suite (STS: https://spring.io/tools) es una extensión para IDEs ampliamente utilizados como Visual Studio Code, Eclipse o Theia y cuenta con muchos complementos relacionados con el framework Spring.

3.3. Usando IntelliJ IDEA Ultimate

IntelliJ IDEA (en su versión Ultimate) tiene soporte para Spring Boot. La versión Ultimate está disponible gratuitamente para estudiantes y educadores: https://www.jetbrains.com/help/idea/spring-boot.html#run-a-spring-boot-application

Puedes crear un proyecto Spring Boot desde IntelliJ IDEA de la siguiente manera:

Nota

El soporte para Spring Framework solo viene con la edición comercial IntelliJ IDEA Ultimate, no con la edición gratuita Community. Si deseas usar la edición Community de IntelliJ IDEA, puedes generar el proyecto mediante Spring Initializr e importarlo en IntelliJ IDEA como un proyecto Maven/Gradle.

3.4. Usando NetBeans IDE

NetBeans IDE no hay soporte integrado para crear proyectos de Spring Boot en NetBeans, pero la comunidad ha construido el complemento NB Spring Boot (consultad: https://github.com/AlexFalappa/nbspringboot), que permite crear aplicaciones de Spring Boot directamente desde el IDE.

Nota: Hay otras opciones para usar rápidamente Spring Boot mediante Spring Boot CLI y SDKMAN. Puedes encontrar más detalles en “Installing Spring Boot” en https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#getting-started.installing.

4. Estructura del Proyecto

Después de descomprimir el archivo ZIP, tendrás una estructura de proyecto básica. Por ejemplo:

Estructura básica de un proyecto (otro) de ejemplo:

├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│   ├── java
│   │   └── com
│   │       └── javhoz
│   │           └── biblioteca
│   │               └── BibliotecaApplication.java
│   └── resources
│       ├── application.properties
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── javhoz
                └── biblioteca
                    └── BibliotecaApplicationTests.java

4.1. El archivo pom.xml

El pom.xml incluye el módulo: spring-bootstarter-parent. Lo primero que debemos tener en cuenta aquí es que el módulo Maven springboot-basic hereda del módulo spring-boot-starter-parent. Al heredar de spring-bootstarter-parent, este nuevo módulo tendrá automáticamente los siguientes beneficios:

Por ejemplo:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>local.sanclemente.ad</groupId>
    <artifactId>springboot-basic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!-- Inherit from Spring Boot Starter Parent. Módulo principal de Spring Boot -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
    </parent>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.3</version>
        </dependency>

        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Veremos más adelante el spring-boot-maven-plugin.

Por ejemplo selecciona solo el starter web (spring-boot-starter-web), pero el starter de prueba (spring-boot-starter-test) también se incluye de forma predeterminada. Se seleccionó la versión 17 o 21 de Java, de ahí que la propiedad <java.version>21</java.version> esté incluida. Este valor de java.version se utilizará para configurar la versión de JDK para el compilador Maven en el módulo spring-boot-starter-parent.

<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>

4.2. La clase principal de la aplicación

El módulo Spring Boot tipo JAR generado tendrá una clase Java de punto de entrada de la aplicación llamada SpringbootBasicApplication.java (el proyecto fue nombrado springboot-basic). Esta clase es una clase de configuración de Spring Boot anotada con @SpringBootApplication y tiene un método public static void main(String[] args), que puedes usar para ejecutar la aplicación.

// local.sanclemente.ad.demo.SpringbootBasicApplication.java
package local.sanclemente.ad.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootBasicApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootBasicApplication.class, args);
    }
}

Aquí, la clase SpringbootBasicApplication está anotada con la anotación @SpringBootApplication, que es una anotación compuesta. Como puedes ver en el código fuente de la anotación @SpringBootApplication, es una combinación de varias anotaciones: @SpringBootConfiguration, @EnableAutoConfiguration o @ComponentScan.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
    // ... (contenido omitido para brevedad)
}

La anotación @SpringBootConfiguration es otra anotación compuesta con la anotación @Configuration.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

Aquí están los significados de estas anotaciones:

Estás iniciando la aplicación llamando a SpringApplication.run(SpringbootBasicApplication.class, args) en el método main(). Puedes pasar una o más clases de configuración de Spring dentro del método SpringApplication.run(). Pero supongamos que tienes tu clase de punto de entrada de la aplicación en un paquete raíz. En ese caso, es suficiente pasar solo la clase de entrada de la aplicación, que se encarga de escanear otras clases de configuración de Spring en todos los subpaquetes.

4.3. La clase del controladora

Ahora debes crea un controlador sencillo de SpringMVC, llamado HomeController.java, como se muestra:

// **Listado 2-3. HomeController.java**
package local.sanclemente.ad.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController { 
    @RequestMapping("/") // Mapea la URL raíz
    public String home(Model model) {
        return "index.html";
    }
}

La clase anterior es un controlador sencillo de SpringMVC con un método controlador de solicitudes para la URL /, que devuelve la vista llamada index.html.

4.4. La vista HTML

Crea una vista HTML llamada index.html.

Spring Boot sirve el contenido estático desde los directorios src/main/resources/static/. Entonces, crea index.html en src/main/resources/static, como se muestra en:

<!-- **index.html** -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Home</title>
</head>
<body>
    <h2>Ola mundo!!</h2>
</body>
</html>

Ahora, desde el IDE, ejecuta el método main() de SpringbootBasicApplication como una clase Java independiente que iniciará el servidor Tomcat integrado en el puerto 8080 y apunta el navegador a http://localhost:8080/. Deberías poder ver la respuesta: “Ola mundo!!”

También puedes ejecutar la aplicación Spring Boot usando spring-boot-maven-plugin, de la siguiente manera:

mvn spring-boot:run
/*
 Clase principal Application.java en el paquete raíz.
 */
package com.micompanhia.miproyecto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Es muy recomendable colocar la clase principal de entrada en el paquete raíz, por ejemplo, en com.micompanhia.miproyecto, de modo que las anotaciones @EnableAutoConfiguration y @ComponentScan escaneen automáticamente los beans de Spring, entidades JPA, y similares en el paquete raíz y todos sus subpaquetes.

Si tienes una clase de punto de entrada en un paquete anidado, es posible que necesites especificar explícitamente los basePackages para escanear los componentes de Spring, como se muestra:

/**
 * Clase principal Application.java en un paquete NO raíz.
 */

package com.micompanhia.miproyecto.config;

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.micompanhia.miproyecto")
@EntityScan(basePackageClasses = Person.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Aquí, la clase principal Application.java está en el paquete com.micompanhia.miproyecto.config, que no es el paquete raíz. Por lo tanto, se necesita especificar @ComponentScan(basePackages = "com.micompanhia.miproyecto") para que Spring Boot escanee com.micompanhia.miproyecto y todos sus subpaquetes en busca de componentes de Spring.

Además, debes ==indicar @EntityScan(basePackageClasses=Persona.class) para que Spring Boot escanee entidades JPA bajo el paquete donde existe Persona.class=0.

4.5. Fat JAR usando el complemento Maven Spring Boot

Puedes ejecutar tu aplicación directamente desde el IDE o usar maven spring-boot:run durante el desarrollo, pero finalmente necesitas crear una unidad de implementación que pueda ejecutarse en el entorno de producción sin ningún soporte del IDE. Puedes usar spring-boot-maven-plugin para crear una única unidad de implementación (un Fat JAR) ejecutando los siguientes objetivos de Maven:

mvn clean package

En el directorio target hay dos archivos importantes: springboot-basic-1.0-SNAPSHOT.jar y springboot-basic-1.0-SNAPSHOT.jar.original:

Puedes crear unidades de implementación autocontenidas para módulos de tipo JAR usando complementos como maven-shade-plugin, que empaqueta todas las clases de JAR dependientes en un solo archivo JAR. Pero Spring Boot sigue un enfoque diferente y te permite anidar JAR directamente dentro de tu archivo JAR de aplicación Spring Boot. Puedes obtener más información en http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#executable-jar.

Puedes ejecutar la aplicación con el siguiente comando:

java -jar springboot-basic-1.0-SNAPSHOT.jar

5. Cómo Ejecutar la Aplicación

5.1. Estructura de un proyecto Spring Boot

Estructura básica del proyecto.

├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│   ├── java
│   │   └── com
│   │       └── javhoz
│   │           └── biblioteca
│   │               └── BibliotecaApplication.java
│   └── resources
│       ├── application.properties
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── javhoz
                └── biblioteca
                    └── BibliotecaApplicationTests.java

Estructura del Proyecto Creado Usando Spring Initializr

La estructura de carpetas es similar a cualquier proyecto Maven o Gradle, pero hay pequeños cambios:

Configuración de application.properties: https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html

Si lo prefieres, como application.properties está vacío, se puede eliminar ese archivo y crear un nuevo archivo, application.yml, con el contenido del Listado siguiente, que define los endpoints del actuador que la aplicación expone, el puerto y la URL predeterminada. La razón de hacer esto es reducir la complejidad del archivo de configuración para entender la jerarquía de propiedades, pero puedes hacer lo mismo con application.properties sin problemas.

management:
  endpoints:
    web:
      base-path: /
      exposure:
        include: "*" # Indica que todos los endpoints están expuestos.
  server:
    port: 8080 # Indica el puerto predeterminado de la aplicación
  servlet:
    context-path: /api/biblioteca # Indica la URL predeterminada

Configuración con la URL Predeterminada y los Endpoints Exponenciales.

5.2. Ejecución de la Aplicación

Hay dos formas de ejecutar la aplicación: usar el IDE o la línea de comandos, que es la opción siguiente:

./mvnw spring-boot:run

Al ejecutar una salida similar al listado siguiente, con toda la información sobre la ubicación de la aplicación que se inicia, el servidor contenedor que utiliza la aplicación y otros detalles adicionales. La información más relevante de esta salida es el puerto y la URL predeterminada (/api/biblioteca), que siempre debes verificar en caso de que algo pueda estar mal.

/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0-M4)
2024-04-15T11:05:12.370-03:00 INFO 1172745 --- [ restartedMain]
com.javhoz.biblioteca.ApiBibliotecaApplication : Starting
ApiBibliotecaApplication using Java 17.0.4 on pepecalo with PID 1172734
(/home/pepecalo/Codigo/api-biblioteca/target/classes started by pepecalo
in /home/pepecalo/Codigo/api-biblioteca )
2024-04-19T11:05:12.375-03:00 INFO 1172745 --- [ restartedMain]
com.javhoz.biblioteca.ApiBibliotrecaApplication : No active profile set,
falling back to 1 default profile: "default"
2024-04-19T11:05:12.425-03:00 INFO 1172745 --- [ restartedMain]
.e.DevToolsPropertyDefaultsPostProcessor : Devtools property
defaults active! Set 'spring.devtools.add-properties' to 'false'
to disable
2022-09-19T11:05:12.426-03:00 INFO 1172745 --- [ restartedMain]

5.3. Ejecutar la aplicación desde consola

Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.

package com.micompanhia.miproyecto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Existen varias formas de hacerlo:

@SpringBootApplication
public class MiAplicacion implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MiAplicacion implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
@Component
public class MyComponent {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}

@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.

@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.

@SpringBootApplication
public class MiAplicacion {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.

Diferencias entre ejecutores de código al inicio de la Aplicación

Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:

ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

ApplicationRunner recoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().

CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.

Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.

Puedes encontrar más información con ejemplos en: Guía para Ejecutar Lógica en el Inicio en Spring.

02. Spring Data JPA.

0. JPA, Hibernate y Spring Data

0.1. JPA (Jakarta Persistence API) y Hibernate

Características JPA

La especificación JPA define lo siguiente:

Hibernate implementa JPA y respalda todos los mapeos, consultas y interfaces de programación estandarizados.

Ventajas de Hibernate

0.2. Spring Data

Spring Data es una familia de proyectos pertenecientes al framework Spring cuyo propósito es simplificar el acceso tanto a bases de datos relacionales como a bases de datos NoSQL. Proporciona los elementos fundamentales del framework Spring que poseen todos los módulos de Spring Data.

Spring Data JPA es un subproyecto de Spring Data que simplifica el acceso a datos JPA. Spring Data JPA elimina la necesidad de escribir consultas SQL. En su lugar, puedes definir consultas personalizadas en el repositorio y Spring Data JPA generará las consultas SQL por ti.

Spring Data Spring Data

// Ejemplo de interfaz de repositorio Spring Data Commons
public interface RepositorioComun<T, ID> extends Repository<T, ID> {
    // Métodos de repositorio común
    Optional<T> findById(ID id);
    T save(T entity);
    // Otros métodos...
}
// Ejemplo de Entidad JPA:
@Entity
public class Entidad {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String nombre;
    // Otros atributos...
    // Getters y setters...
}
// Ejemplo de interfaz de repositorio Spring Data JPA
public interface RepositorioJPA extends JpaRepository<Entidad, Long> {
    // Métodos de repositorio JPA
    List<Entidad> findByNombre(String nombre);
    // Otros métodos persomalozados si es necesario...
}
// Ejemplo de Entidad JDBC:
public class Entidad {
    private Long id;
    private String nombre;
    // Otros atributos...
    // Getters y setters...
}

// Ejemplo de interfaz de repositorio Spring Data JDBC
public interface RepositorioJDBC extends CrudRepository<Entidad, Long> {
    // Métodos de repositorio JDBC
    List<Entidad> findByNombre(String nombre);
    // Otros métodos persomalozados si es necesario...
}
    // Ejemplo de interfaz de repositorio Spring Data REST 
    @RepositoryRestResource(collectionResourceRel = "entidades", path = "entidades")
    public interface RepositorioREST extends PagingAndSortingRepository<Entidad, Long> {
        // Métodos de repositorio REST
        List<Entidad> findByNombre(String nombre);
        // Otros métodos persomalozados si es necesario...
    }
// Ejemplo de Entidad MongoDB:
@Document(collection = "entidadesMongo")
public class EntidadMongo {
    @Id
    private String id;
    private String nombre;
    // Otros atributos...
    // Getters y setters...
}

// Ejemplo de interfaz de repositorio Spring Data MongoDB
public interface RepositorioMongo extends MongoRepository<EntidadMongo, String> {
    // Métodos de repositorio MongoDB
    List<EntidadMongo> findByNombre(String nombre);
    // Otros métodos persomalozados si es necesario...
}
// Ejemplo de Entidad Redis:
@RedisHash("entidadesRedis")
public class EntidadRedis {
    @Id
    private String id;
    private String nombre;
    // Otros atributos...
    // Getters y setters...
}

// Ejemplo de interfaz de repositorio Spring Data Redis

public interface RepositorioRedis extends CrudRepository<EntidadRedis, String> {
    // Métodos de repositorio Redis
    List<EntidadRedis> findByNombre(String nombre);
    // Otros métodos persomalozados si es necesario...
}

El código fuente de Spring Data (junto con otros proyectos de Spring) se puede descargar libremente desde https://github.com/spring-projects.

Para aprovechar Hibernate de manera eficaz, es necesario poder ver e interpretar las declaraciones SQL que emite y comprender sus implicaciones de rendimiento. Para beneficiarse de las ventajas de Spring Data, es esencial anticipar cómo se crean el código estándar y las consultas generadas.

0.2.1. API de Spring Data

La API DE String Data puede consultarse en la página:

https://javadoc.io/doc/org.springframework.data

0.3. Ventajas de Spring Data

Spring Data y JPA

Ventajas de Spring Data

1. Repositorios de Spring Data

Spring Data JPA proporciona una interfaz de repositorio que extiende la interface ListCrudRepository<T,ID> y de ListPagingAndSortingRepository<T,ID>. CrudRepository (y su versión con listas) proporciona métodos CRUD básicos, métodos para paginación. Además, Spring Data JPA proporciona métodos adicionales para realizar consultas personalizadas.

Spring Data JPA también proporciona métodos para paginación y ordenación. Spring Data JPA generará consultas SQL para estos métodos.

Spring Data JPA proporciona soporte para capas de acceso a datos basadas en JPA al reducir el código repetitivo y crear implementaciones para las interfaces de repositorio.

Solo necesitamos definir nuestra propia interfaz de repositorio, extendiendo una de las interfaces de Spring Data.

El conjunto de interfaces de repositorio de Spring Data comunes es el siguiente:

Jerarquía de interfaces de repositorio de Spring Data Jerarquía de interfaces de repositorio de Spring Data

Ejemplo de uso de PagingAndSortingRepository:

public interface RepositorioPaginacion extends PagingAndSortingRepository<Entidad, Long>, CrudRepository<Entidad, Long>{
    // Métodos de repositorio con paginación y ordenación
    List<Entidad> findByNombre(String nombre, Pageable pageable);
    // Otros métodos persomalozados si es necesario...
}

Por heredar de PagingAndSortingRepository, el repositorio RepositorioPaginacion tendrá acceso a los métodos de paginación y ordenación: findAll(Sort sort) y findAll(Pageable pageable).

Interfaces específicas de JPA:

Otros repositorios de Spring Data son:

// Ejemplo de interfaz de repositorio Spring Data Redis
public interface RepositorioRedis extends CrudRepository<EntidadRedis, String> {
    // Métodos de repositorio Redis
    List<EntidadRedis> findByNombre(String nombre);
    // Otros métodos personalizados si es necesario...
}

¿Qué es Spring Data Solr?

a) Configuración básica:

Dependencia Maven:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-solr</artifactId>
    <version>4.3.14</version>
</dependency>

Declaramos un documento Solr llamado Producto, por ejemplo, que queremos almacenar en Solr:

import org.springframework.data.annotation.Id;
import org.springframework.data.solr.core.mapping.Indexed;
import org.springframework.data.solr.core.mapping.SolrDocument;

@SolrDocument(solrCoreName = "product")
public class Producto {
  @Id
  @Indexed(name = "id", type = "string")
  private String id;

  @Indexed(name = "name", type = "string")
  private String name;

  // Getters y setters (omito para simplificar)
}

El repositorio ProductoRepositorio:

import org.springframework.data.solr.repository.SolrCrudRepository;

public interface ProductoRepositorio extends SolrCrudRepository<Producto, String> {
  List<Producto> findByName(String name);
}

Configuración de Spring (se precisa configurar la capa de persistencia de la aplicación Spring):

import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.solr.core.SolrTemplate;
import org.springframework.data.solr.repository.config.EnableSolrRepositories;

@Configuration
@EnableSolrRepositories(basePackages = "com.javhoz.ad.repositories", namedQueriesLocation = "classpath:solr-named-queries.properties")
public class SpringDataSolrConfiguration {
    
    @Bean
    public SolrClient solrClient() {
        return new HttpSolrClient.Builder("http://localhost:8983/solr").build();
    }

    @Bean
    public SolrTemplate solrTemplate() {
      return new SolrTemplate(solrClientFactory());
    }
}

Además, se precisa un archivo solr-named-queries.properties en el directorio resources con las consultas personalizadas:

Producto.findByName = name:?0

1.1. Creación de un repositorio

Dada la Entidad Mensaje:

@Entity
public class Mensaje {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String texto;
    // Getters y setters...
}

Por ejemplo, la interface, MensajeRepositorio.java

public interface MensajeRepositorio extends CrudRepository<Mensaje, Long> {
}

La interfaz MensajeRepositorio extiende CrudRepository<Mensaje, Long>. Esto significa que es un repositorio de entidades Mensaje con un identificador Long. Recordemos que la clase Mensaje tiene un campo id anotado como @Id de tipo Long. Podemos llamar directamente a métodos como save, findAll o findById, heredados de CrudRepository, y podemos usarlos sin ninguna información adicional para ejecutar operaciones habituales contra una base de datos. Spring Data JPA creará una clase proxy que implementa la interfaz MensajeRepositorio e implementará sus métodos:

https://springframework.guru/gang-of-four-design-patterns/proxy-pattern/

Ahora, guardemos un Mensaje en la base de datos usando Spring Data JPA: HolaMundoSpringDataJPA.java:

@ExtendWith(SpringExtension.class) // #A
@ContextConfiguration(classes = {SpringDataConfiguration.class}) // #B
public class HolaMundoSpringDataJPA {
    @Autowired // #C
    private MensajeRepositorio repositorioMensaje; // #C

    @Test
    public void saveMensaje() {
        Mensaje message = new Mensaje(); // #D
        message.setText("Hola MUndo desde Spring Data JPA!"); // #D
        repositorioMensaje.save(message); // #E

        List<Mensaje> mensajes = (List<Mensaje>)repositorioMensaje.findAll(); // #F
        assertAll( // #G
            () -> assertEquals(1, mensajes.size()), // #G
            () -> assertEquals("Hola MUndo desde Spring Data JPA!", mensajes.get(0).getText()) // #H
        );
    }
}

/*
    #A Extendemos la prueba utilizando SpringExtension. Esta extensión se utiliza para integrar el contexto de prueba de Spring con JUnit 5 Jupiter: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/context/junit/jupiter/SpringExtension.html.
    #B La configuración del contexto de prueba de Spring se realiza utilizando los beans definidos en la clase SpringDataConfiguration.
    #C Se inyecta un bean MensajeRepositorio mediante "auto-cableado" de Spring. Esto es posible gracias a que el paquete com.javhoz.ad.repositories, donde se encuentra MensajeRepositorio, se usó como argumento en la anotación @EnableJpaRepositories. Si llamamos a repositorioMensaje.getClass(), veremos algo como com.sun.proxy.$Proxy41, un proxy generado por Spring Data.
    #D Creamos una nueva instancia de la clase de modelo de dominio mapeada Mensaje y establecemos su propiedad de texto.
    #E Persistimos el objeto mensaje. El método save se hereda de la interfaz CrudRepository y su cuerpo será generado por Spring Data JPA cuando se cree la clase proxy. Simplemente guardará una entidad Mensaje en la base de datos.
    #F Recuperamos los mensajes del repositorio. El método findAll se hereda de la interfaz CrudRepository y su cuerpo será generado por Spring Data JPA cuando se cree la clase proxy. Simplemente devolverá todas las entidades pertenecientes a la clase Mensaje.
    #G Verificamos el tamaño de la lista de mensajes recuperados de la base de datos y que el mensaje que persistimos está en la base de datos #H. Utilizamos el método assertAll de JUnit 5, que siempre verifica todas las afirmaciones que se le pasan, incluso si algunas de ellas fallan. Las dos afirmaciones que verificamos están conceptualmente relacionadas.
*/

La prueba de Spring Data JPA es considerablemente más corta que las que utilizan JPA o Hibernate nativo. Esto se debe a que se ha eliminado el código redundante, no hay más creación explícita de objetos ni control explícito de las transacciones. El objeto del repositorio se inyecta y proporciona los métodos generados de la clase proxy. La carga es más pesada ahora en el lado de la configuración, pero esto debería hacerse solo una vez por aplicación (que normalmente se ejecuta en un servidor de aplicaciones y no he puesto en el ejemplo).

1.2. Añadiendo Métodos a la Interfaz a un repositorio en Spring Data JPA

Supongamos una clase de modelo de dominio Usuario:

@Entity
public class Usuario {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String nombre;
    private String email;
    private LocalDate fechaRegistro;
    private boolean activo;
    private int nivel;
    // Getters y setters...
}

Comenzaremos a agregar nuevos métodos a la interfaz RepositorioUsuario y los utilizaremos en pruebas recién creadas.

La interfaz RepositorioUsuario ahora extenderá JpaRepository** en lugar de CrudRepository.

JpaRepository extiende PagingAndSortingRepository yCrudRepository. En realidad hereda de las subclases: ListCrudRepository que devuelven List en lugar de Iterable y ListPagingAndSortingRepository que devuelven List en lugar de Iterable y añaden paginación y ordenación. JpaRepository también agrega métodos específicos de JPA.

También agregaremos una serie de métodos de consulta a la interfaz RepositorioUsuario:

// RepositorioUsuario.java
public interface RepositorioUsuario extends JpaRepository<Usuario, Long> {
    Usuario findByNombre(String username);
    List<Usuario> findAllByOrderByNombreAsc();
    List<Usuario> findByFechaRegistroBetween(LocalDate start, LocalDate end);
    List<Usuario> findByNombreAndEmail(String username, String email);
    List<Usuario> findByNombreOrEmail(String username, String email);
    List<Usuario> findByNombreIgnoreCase(String username);
    List<Usuario> findByNivelOrderByNombreDesc(int level);
    List<Usuario> findByNivelGreaterThanEqual(int level);
    List<Usuario> findByNombreContaining(String text);
    List<Usuario> findByNombreLike(String text);
    List<Usuario> findByNombreStartingWith(String start);
    List<Usuario> findByNombreEndingWith(String end);
    List<Usuario> findByActivo(boolean activo);
    List<Usuario> findByFechaRegistroIn(Collection<LocalDate> dates);
    List<Usuario> findByFechaRegistroNotIn(Collection<LocalDate> dates);
}

El propósito de los métodos de consulta es recuperar información de la base de datos.

Spring Data JPA proporciona un mecanismo de construcción de consultas que creará el comportamiento de los métodos del repositorio según sus nombres. Analizaremos más adelante las consultas de modificación; ahora nos sumergiremos en las consultas cuyo propósito es encontrar información.

Este mecanismo de consulta elimina los prefijos y sufijos como find...By, get...By, query...By, read...By, count...By del nombre del método y analiza el resto.

Métodos de consulta de Spring Data JPA: https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

Los nombres de los métodos deben seguir las reglas para determinar la consulta resultante. Si el nombre del método es incorrecto (por ejemplo, la propiedad de la entidad no coincide en el método de consulta), recibirás un error al cargar el contexto de la aplicación.

La tabla siguiente describe las palabras clave que Spring Data JPA admite y cómo se transpone cada nombre de método en JPQL.

Palabra clave Ejemplo Fragmento JPQL
Distinct findDistinctByLastnameAndFirstname select distinct …​ where x.lastname = ?1 and x.firstname = ?2
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is, Equals findByFirstname, findByFirstnameIs, findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull, Null findByAge(Is)Null … where x.age is null
IsNotNull, NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

Además, Spring Data JPA examinará el tipo de retorno del método. Si deseas encontrar un Usuario y devolverlo en un contenedor Optional, el tipo de retorno del método será Optional<Usuario>. Se puede encontrar una lista completa de tipos de retorno posibles, junto con explicaciones detalladas en: https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html.

2. Fichero de configuración application.properties

El fichero de configuración de Spring Boot se llama application.properties y es el lugar donde se definen las propiedades de la aplicación. Spring Boot busca automáticamente un archivo application.properties en el directorio src/main/resources. Si no se encuentra, se utilizarán las propiedades por defecto.

Por ejemplo, para configurar una base de datos PostgreSQL, el archivo application.properties podría ser:

spring.application.name=Nombre de la aplicación
#spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:postgresql://localhost:5432/mibasededatos
spring.datasource.username=usuario
spring.datasource.password=contraseña
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.show-sql=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.globally_quoted_identifiers=true
#logging.level.org.hibernate.SQL=DEBUG
#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

Estrategia de nombres

Hibernate utiliza nombres de campo mediante una estrategia física y una estrategia implícita.

Y Spring Boot proporciona valores por defecto para ambos:

Podemos sobrescribir estos valores, pero de forma predeterminada, Spring Boot utiliza las estrategias de nombres de Hibernate:

Identificadores entre comillas

Como SQL es un lenguaje declarativo, las palabras reservadas de la gramática son para uso intenterno y no pueden ser empleadas cuando declramos identificadores de la base de datos (catalogos, esquemas, tablas, columnas, nombres…)

Podemos hacer un escape manual de las palabras reservadas con comillas dobles, pues Hibernate no lo hace por defecto:

@Entity
@Table(name = "\"User\"")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
    // Getters y setters...
}

Para que Hibernate utilice comillas dobles, debemos configurar la propiedad hibernate.globally_quoted_identifiers a true.

También puede escaparse con el carácter específico de hibernate:

@Entity
@Table(name = "`User`")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    private String email;
    // Getters y setters...
}

La tercera opción es poniendo la opción hibernate.globally_quoted_identifiers a true en el fichero application.properties:. De esta forma Hibernate escapará todos los identificadores con comillas dobles.

Para poder usar identificadores entre comillas en las consultas de Spring Data JPA, debemos configurar la estrategia de nombre físico de Hibernate a org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.

spring.jpa.properties.hibernate.globally_quoted_identifiers=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

Ejercicio. Spring de Vehículos

Crea una aplicación en Spring Boot gestionar una base de datos de vehículos. La base de datos tiene las siguientes entidades:

Hazlo con la estrategia de herencia de JPA de JOINED.

Crea una nueva clase Vehiculo que se llame VehiculoDTO que tenga los campos:

Crea las entidades JPA correspondientes y realiza las consultas necesarias para obtener los vehículos de la base de datos, creando un repositorio para cada entidad.

Hazlo con una base de datos H2 orientada a fichero.

La interface VehiculoRepository debe tener, al menos los siguientes métodos:

03. Práctica con de Spring Data JPA y REST.

Práctica con de Spring Data JPA y REST

Arquitectura de SpringBoot Arquitectura de SpringBoot

1. Accediendo a Datos JPA con REST

En este ejemplo crearemos una aplicación que accede a datos JPA relacionales a través de una interfaz frontal RESTful basada en Web.

2. Objetivo

Construir una aplicación Spring que te permite crear y recuperar objetos Persona almacenados en una base de datos utilizando Spring Data REST. Spring Data REST combina automáticamente las características de Spring HATEOAS y Spring Data JPA.

Spring Data REST también admite Spring Data Neo4j, Spring Data Gemfire y Spring Data MongoDB como almacenes de datos en el backend.

3. Requisitos

Como sabes, también se puede importar el código directamente en uno de estos IDE:

4. Creación del proyecto

Para inicializar el proyecto manualmente:

  1. Ve a https://start.spring.io. Este servicio incluye todas las dependencias que necesitas para una aplicación y realiza la mayor parte de la configuración por ti.

  2. Elige Maven y Java.

  3. Haz clic en Dependencies y selecciona Rest Repositories, Spring Data JPA, y H2 Database.

  4. Haz clic en Generate.

  5. Descarga el archivo ZIP resultante, que es un archivo de una aplicación web configurada con lo seleccionado.

Si el IDE tiene la integración de Spring Initializr, puedes completar este proceso desde el IDE (Visual Studio Code, Eclipse). También puedes bifurcar el proyecto desde Github y abrirlo en tu IDE u otro editor.

Crear un Objeto de Dominio

Crea una entidad para representar a una Persona, como muestra el siguiente listado (en src/main/java/local/sanclemente/accesorest/Persona.java):

package local.sanclemente.accesorest;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Persona {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  private String nombre;
  private String apellido;

  public String getNombre() {
    return nombre;
  }

  public void setNombre(String nombre) {
    this.nombre = nombre;
  }

  public String getApellido() {
    return apellido;
  }

  public void setApellido(String apellido) {
    this.apellido = apellido;
  }
  // ... getters and setters
}

El objeto Persona tiene un nombre y un apellido. (También hay un objeto ID configurado para generarse automáticamente).

5. Creación de un Repositorio de Persona

Luego, necesitas crear un repositorio sencillo, como muestra el siguiente listado (en src/main/java/local/sanclemente/accesorest/PersonaRepository.java):

package local.sanclemente.accesorest;

import java.util.List;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "gente", path = "gente")
// En realidad podrías emplear directamente JpaRepository que extiende de ListPagingAndSortingRepository y ListCrudRepository
public interface PersonaRepository extends PagingAndSortingRepository<Persona, Long>, CrudRepository<Persona,Long> {

  List<Persona> findByApellido(@Param("nombre") String nombre);

}

Este repositorio es una interfaz que te permite realizar varias operaciones con objetos Persona.

Obtiene estas operaciones al heredar la interfaz PagingAndSortingRepository que está definida en Spring Data Commons.

@RepositoryRestResource no es necesario para que un repositorio se exporte. Se utiliza solo para cambiar los detalles de exportación, como el uso de /gente en lugar del valor predeterminado /Personas.

Aquí también has definido una consulta personalizada para recuperar una lista de objetos Persona basada en el apellido. Puedes ver cómo invocarla más adelante.

@SpringBootApplication es una anotación de conveniencia que agrega lo siguiente:

El método main() utiliza el método SpringApplication.run() de Spring Boot para iniciar una aplicación. ¿Notaste que no hubo una sola línea de XML? Tampoco hay un archivo web.xml. Esta aplicación web es 100% Java puro y no tuviste que lidiar con la configuración de la infraestructura.

Spring Boot inicia automáticamente Spring Data JPA para crear una implementación concreta de PersonaRepository y la configura para hablar con una base de datos en memoria utilizando JPA.

Spring Data REST se basa en Spring MVC. Crea una colección de controladores Spring MVC, convertidores JSON y otros beans para proporcionar un frente RESTful. Estos componentes se vinculan al backend de Spring Data JPA. Cuando usas Spring Boot, todo esto está autoconfigurado. Si deseas investigar cómo funciona, puedes mirar la clase RepositoryRestMvcConfiguration en Spring Data REST.

6. Construir un JAR ejecutable

Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven.

También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias necesarias, clases y recursos y ejecutar eso. Construir un JAR ejecutable facilita su envío, versión e implementación del servicio como una aplicación a lo largo del ciclo de desarrollo, en diferentes entornos, y así sucesivamente.

Si usas Gradle, puedes ejecutar la aplicación con ./gradlew bootRun. Alternativamente, puedes construir el archivo JAR con ./gradlew build y luego ejecutar el archivo JAR, así:

java -jar build/libs/{project_id}-0.1.0.jar

Si usas Maven, puedes ejecutar la aplicación con ./mvnw spring-boot:run. Alternativamente, puedes construir el archivo JAR con ./mvnw clean package y luego ejecutar el archivo JAR, así:

java -jar target/{project_id}-0.1.0.jar

Los pasos descritos aquí crean un JAR ejecutable. También puedes construir un archivo WAR clásico.

Se muestra la salida de registro. El servicio debería estar en funcionamiento en unos pocos segundos.

7. Probar la Aplicación

Ahora que la aplicación está en funcionamiento, puedes probarla. Puedes usar cualquier cliente REST que desees (o un navegador). Los siguientes ejemplos usan la herramienta *nix, curl (puedes descargar la versión portable de Windows desde aquí).

Primero, quieres ver el servicio de nivel superior. El siguiente ejemplo muestra cómo hacerlo:

$ curl http://localhost:8080
{
  "_links" : {
    "gente" : {
      "href" : "http://localhost:8080/gente{?page,size,sort}",
      "templated" : true
    }
  }
}

El ejemplo anterior proporciona una primera visión de lo que ofrece este servidor. Hay un enlace gente ubicado en http://localhost:8080/gente. Tiene algunas opciones, como ?page, ?size, y ?sort.

Spring Data REST utiliza el formato HAL para la salida JSON. Es flexible y ofrece una forma conveniente de suministrar enlaces junto a los datos que se sirven.

El siguiente ejemplo muestra cómo ver los registros de Persona (ninguno por el momento):

$ curl http://localhost:8080/gente
{
  "_embedded" : {
    "gente" : []
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente{?page,size,sort}",
      "templated" : true
    },
    "search" : {
      "href" : "http://localhost:8080/gente/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0,
    "number" : 0
  }
}

7.1. Creación de un registro

Actualmente, no hay elementos y, por lo tanto, no hay páginas. Tenemos que crear personas, de tipo Persona:

$ curl -i -H "Content-Type:application/json" -d '{"nombre": "Frodo", "apellido": "Baggins"}' http://localhost:8080/gente
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Location: http://localhost:8080/gente/1
Content-Length: 0
Date: Wed, 26 Feb 2014 20:26:55 GMT

Si estás en Windows, el comando anterior funcionará en WSL. Si no puedes instalar WSL, es posible que necesites reemplazar las comillas simples con comillas dobles y escapar las comillas dobles existentes, es decir, -d "{\"nombre\": \"Frodo\", \"apellido\": \"Baggins\"}".

Observa cómo la respuesta a la operación POST incluye una cabecera Location. Esto contiene la URI del recurso recién creado.

Spring Data REST también tiene dos métodos (RepositoryRestConfiguration.setReturnBodyOnCreate(…) y setReturnBodyOnUpdate(…)) que puedes usar para configurar el marco para que devuelva inmediatamente la representación del recurso recién creado. RepositoryRestConfiguration.setReturnBodyForPutAndPost(…) es un método abreviado para habilitar respuestas de representación para operaciones de creación y actualización.

7.2. Consulta de un registro

Puedes consultar todas las Persona, como muestra el siguiente ejemplo:

$ curl http://localhost:8080/gente
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente{?page,size,sort}",
      "templated" : true
    },
    "search" : {
      "href" : "http://localhost:8080/gente/search"
    }
  },
  "_embedded" : {
    "gente" : [ {
      "nombre" : "Frodo",
      "apellido" : "Baggins",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/gente/1"
        }
      }
    } ]
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages"

 : 1,
    "number" : 0
  }
}

El objeto gente contiene una lista que incluye a Frodo. Observa cómo incluye un enlace self. Spring Data REST también utiliza Evo Inflector para pluralizar el nombre de la entidad para agrupamientos.

Puedes consultar directamente el registro individual, así:

$ curl http://localhost:8080/gente/1
{
  "nombre" : "Frodo",
  "apellido" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente/1"
    }
  }
}

Esto podría parecer puramente basado en la web. Sin embargo, detrás de escena, hay una base de datos relacional H2. En producción, recomendaría usar PostgreSQL.

Con un sistema más complejo, donde los objetos de dominio están relacionados entre sí, Spring Data REST renderiza enlaces adicionales para ayudar a navegar a registros conectados.

Puedes realizar todas las consultas personalizadas, como se muestra en el siguiente ejemplo:

$ curl http://localhost:8080/gente/search
{
  "_links" : {
    "findByApellido" : {
      "href" : "http://localhost:8080/gente/search/findByApellido{?nombre}",
      "templated" : true
    }
  }
}

Puedes ver la URL de la consulta, incluido el parámetro de consulta HTTP, nombre. Ten en cuenta que esto coincide con la anotación @Param("nombre") incrustada en la interfaz.

El siguiente ejemplo muestra cómo usar la consulta findByApellido:

$ curl http://localhost:8080/gente/search/findByApellido?nombre=Baggins
{
  "_embedded" : {
    "Personas" : [ {
      "nombre" : "Frodo",
      "apellido" : "Baggins",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/gente/1"
        }
      }
    } ]
  }
}

Debido a que la has definido para devolver List<Persona> en el código, devuelve todos los resultados. Si la hubieras definido para devolver solo Persona, elegiría uno de los objetos Persona para devolver. Dado que esto puede ser impredecible, probablemente es mejor no hacerlo para consultas que puedan devolver múltiples entradas.

7.3. Actualización de registros

También se pueden emitir llamadas REST de PUT, PATCH y DELETE para reemplazar, actualizar o eliminar registros existentes (respectivamente). El siguiente ejemplo usa una llamada PUT:

$ curl -X PUT -H "Content-Type:application/json" -d '{"nombre": "Bilbo", "apellido": "Baggins"}' http://localhost:8080/gente/1
$ curl http://localhost:8080/gente/1
{
  "nombre" : "Bilbo",
  "apellido" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente/1"
    }
  }
}

El siguiente ejemplo usa una llamada PATCH:

$ curl -X PATCH -H "Content-Type:application/json" -d '{"nombre": "Bilbo Jr."}' http://localhost:8080/gente/1
$ curl http://localhost:8080/gente/1
{
  "nombre" : "Bilbo Jr.",
  "apellido" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente/1"
    }
  }
}

PUT reemplaza todo el registro. Los campos no suministrados se reemplazan con null.

Puedes usar PATCH para actualizar un subconjunto de elementos.

7.4. Eliminación de registros

También puedes eliminar registros, como muestra el siguiente ejemplo:

$ curl -X DELETE http://localhost:8080/gente/1
$ curl http://localhost:8080/gente
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente{?page,size,sort}",
      "templated" : true
    },
    "search" : {
      "href" : "http://localhost:8080/gente/search"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0,
    "number" : 0
  }
}

Un aspecto conveniente de esta interfaz basada en hipertexto es que puedes descubrir todos los puntos finales RESTful usando curl (o cualquier cliente REST que prefieras). No es necesario intercambiar un contrato o documento de interfaz formal con tus clientes.

04. Repositorios String Data.

1. Repositorios

Los repositorios son la abstracción que Spring Data utiliza para interactuar con las bases de datos, reduciendo la cantidad de bloques de código en tu aplicación.

Las clases DAO que interactúan con la base de datos y mapean los resultados, todo en la misma capa, pero son demasiado grandes y complejos de seguir la lógica.

Los repositorios no incluyen código de lógica de negocio, solo la declaración de los métodos para interactuar con la base de datos.

Spring Data ofrece una lista de repositorios (todos los cuales son interfaces que puedes extender), indicando la entidad y su tipo de ID. En tiempo de ejecución, el framework crea una clase proxy con toda la lógica necesaria para acceder a la base de datos.

En el ejemplo, BookRepository extiende de CrudRepository<T, ID>, que proporciona un conjunto de métodos para ejecutar operaciones CRUD en la base de datos:

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.javhoz.biblioteca.model.Book;

public interface BookRepository extends CrudRepository<Book, Long> {
    
}

Algunos de los métodos de esta interfaz padre son:

package org.springframework.data.repository;
import java.util.Optional;
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity); // Guarda o actualiza la entidad
    Optional<T> findById(ID primaryKey);
    Iterable<T> findAll();
    long count();
    void delete(T entity);
    boolean existsById(ID primaryKey);
    // ... otros métodos más.
}

Otra interfaz es PagingAndSortingRepository<T, ID>, que extiende de CrudRepository, JpaRepository y MongoRepository. Cada uno de los dos últimos repositorios se utiliza para un tipo específico de base de datos en lugar de CrudRepository y PagingAndSortingRepository, que son interfaces genéricas para todas las bases de datos.

En Spring Data 3.0.0, apareció un nuevo repositorio. ListCrudRepository<T, ID> incluye métodos adicionales para recuperar una lista de elementos en lugar de una interfaz Iterable.

Los métodos que proporcionan los repositorios más comunes son útiles en la mayoría de los casos. Pero ¿qué sucede si necesitas encontrar un elemento por otra propiedad, no solo por el ID? Spring Data proporciona un mecanismo para crear consultas personalizadas sin crear clases adicionales, simplemente definiendo un método en la interfaz del repositorio con un nombre específico.

1.1. Consultas personalizadas automáticas

Spring Data analiza cada repositorio, buscando todos los métodos definidos para generar una consulta particular para cada uno de ellos. Si necesitas una consulta específica, puedes definir un nuevo método en la interfaz utilizando palabras clave que Spring Data utiliza para crear la consulta.

En este caso, necesitas crear un método que use findBy o existBy, seguido del nombre del campo que deseas buscar en la tabla. Spring lanza una excepción si el atributo no existe en la tabla:

@Repository
public interface BookRepository extends CrudRepository<Book, Long> {
    List<Book> findByIsbn(String isbn);
}

Otras palabras clave permiten crear un conjunto de consultas combinando atributos, limitando la cantidad de resultados, u ordenando de una manera particular (ver Tabla).

La estructura de la consulta se divide en dos partes:

El siguiente ejemplo utiliza una interfaz List en lugar de una interfaz Set para mostrarte que puedes tener elementos duplicados, pero también pudede cambiarse a una interfaz Set, la consulta sigue funcionando sin problemas:

public interface BookRepository extends CrudRepository<Book, Long> {
    // Consultas generales
    List<Book> findByIsbn(String isbn);
    List<Book> findByIsbnAndTitulo(String isbn, String titulo);
    // Consultas con ordenación
    List<Book> findByTituloOrderByIsbnAsc(String descripcion);
    List<Book> findByTituloOrderByIsbnDesc(String descripcion);
}

Podemos ver qué genera Spring Data como consulta para acceder a la base de datos. Todas las aplicaciones cambian el valor de la propiedad show-sql de false a true en application.yml. Si ejecutas la aplicación y haces una solicitud, pueden verse las consultas SQL generadas en la consola.

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
Método Consulta
List findByIsbn(String isbn); select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.isbn=?
List findByIsbnAndTitulo(String isbn, String titulo); select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.isbn=? and book0_.titulo=?
List findByTituloOrderByIsbnAsc(String titulo); select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.titulo=? order by book0_.isbn asc
List findByTituloOrderByIsbnDesc(String titulo); select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.decimal_places as decimal_3_0_, book0_.titulo as descript4_0_, book0_.enabled as enabled5_0_ from book book0_ where book0_.titulo=? order by book0_.isbn desc

Ejemplos de palabras clave sujeto de consultas

Algunas de las palabras clave más comunes son (podrían no ser admitidas por bases de datos no relacionales):

Palabra clave Descripción
findBy… Estas palabras clave están generalmente asociadas con una consulta SELECT y devuelven un elemento o conjunto de elementos que pueden ser un subtipo de Collection o Streamable.
getBy…
queryBy…
countBy… Devuelve el número de elementos que coinciden con la consulta.
existBy… Devuelve un tipo booleano con verdadero si hay algo que coincide con la consulta.
deleteBy… Elimina un conjunto de elementos que coinciden con la consulta pero no devuelve nada.

Ejemplos de palabras clave predicado de consultas

Palabra Clave Expresiones de la Palabra Clave
LIKE Like
IS_NULL Null o IsNull
LESS_THAN LessThan
GREATER_THAN GreaterThan
AND And
OR Or
AFTER After o IsAfter
BEFORE Before o IsBefore

1.2 Consultas personalizadas manuales

La segunda forma de crear consultas para acceder a una base de datos es el método clásico: escribir la consulta que necesitas ejecutar en un formato similar a SQL y definir un método en la interfaz. El repositorio anterior para incluir una consulta manual que encuentra un elemento usando el código:

import com.javhoz.biblioteca.Book;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface BookRepository extends CrudRepository<Book, Long> {
    // Consultas generales
    List<Book> findByIsbn(String code);
    List<Book> findByIsbnAndTitulo(String code, String titulo);

    // Consultas de orden
    List<Book> findByTituloOrderByIsbnAsc(String titulo);
    List<Book> findByTituloOrderByIsbnDesc(String titulo);

    // Consulta manual
    @Query("SELECT c FROM Book c WHERE c.isbn = :isbn")
    Book retrieveByIsbn(@Param("isbn") String isbn);
}

Hay muchas formas de declarar una consulta:

Hay muchas ventajas y desventajas con cada uno de estos enfoques.

¿Por qué necesitas crear una consulta manual si hay una forma de hacerlo automáticamente? Una respuesta es que necesitas mejorar el rendimiento de la consulta que Spring Data genera, o no necesitas todos los atributos de la tabla. Cubres un escenario específico. Esta situación tiene el nombre de Proyecciones. Otra situación es que la consulta es tan compleja que no existe una palabra clave para expresarla. No hay una regla que explique todos los escenarios potenciales cuando necesitas usar un mecanismo en lugar de otro. Pero si sabes que la aplicación tiene un problema con el rendimiento de la consulta, la mejor alternativa podría ser intentar escribir la consulta manualmente y ver qué sucede.

05. Paginación y Ordenación usando Spring Data JPA.

Paginación y Ordenación usando Spring Data JPA

La paginación y la ordenación son características esenciales para manejar grandes conjuntos de datos en aplicaciones. Spring Data JPA ofrece una forma sencilla y poderosa para implementar estas características.

1. Paginación

La paginación te permite dividir grandes resultados en partes más pequeñas y manejables, llamadas páginas. Spring Data JPA proporciona una clase PageRequest para crear solicitudes de paginación:

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/PageRequest.html

Para utilizar la paginación, necesitas extender tu repositorio de PagingAndSortingRepository o JpaRepository. Estas interfaces heredan de CrudRepository y añaden métodos de paginación y ordenación.

Aquí hay un ejemplo de cómo definir un repositorio con paginación:

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MonedaRepository extends PagingAndSortingRepository<Moneda, Long> {
    Page<Moneda> findByDescripcion(String descripcion, Pageable pageable);
}

Luego, puedes utilizar este repositorio en tu servicio:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class MonedaService {
    @Autowired
    private MonedaRepository monedaRepository;

    public Page<Moneda> getMonedasByDescripcion(String descripcion, int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return monedaRepository.findByDescripcion(descripcion, pageable);
    }
}

La interfaz Page proporciona métodos para acceder a la información de la página actual, como el número de elementos, el número total de páginas, etc:

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html

    Page<Moneda> monedas = monedaService.getMonedasByDescripcion("Euro", 0, 10);
    int totalPages = monedas.getTotalPages();
    long totalElements = monedas.getTotalElements();
    List<Moneda> content = monedas.getContent();
    
    for (Moneda moneda : content) {
        System.out.println(moneda);
    }
    
    

Métodos de Page:

Heredados de Slice:

De Iterable:

De Streamable:

2. Ordenación

La ordenación te permite especificar el orden en que se devuelven los resultados. Spring Data JPA facilita la ordenación mediante la clase Sort.

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Sort.html

Puedes especificar la ordenación al crear una instancia de PageRequest o utilizar el método findAll(Sort sort).

Aquí hay un ejemplo de cómo definir un repositorio con ordenación:

import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MonedaRepository extends CrudRepository<Moneda, Long> {
    List<Moneda> findByDescripcion(String descripcion, Sort sort);
}

Luego, puedes utilizar este repositorio en el servicio:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
public class MonedaService {
    @Autowired
    private MonedaRepository monedaRepository;

    public List<Moneda> getMonedasByDescripcionSorted(String descripcion) {
        Sort sort = Sort.by(Sort.Direction.ASC, "codigo");
        return monedaRepository.findByDescripcion(descripcion, sort);
    }
}

3. Combinando Paginación y Ordenación

También puedes combinar paginación y ordenación en una sola solicitud utilizando PageRequest.

Aquí tienes un ejemplo:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
public class MonedaService {
    @Autowired
    private MonedaRepository monedaRepository;

    public Page<Moneda> getMonedasByDescripcionSortedAndPaged(String descripcion, int page, int size) {
        Sort sort = Sort.by(Sort.Direction.ASC, "codigo");
        Pageable pageable = PageRequest.of(page, size, sort);
        return monedaRepository.findByDescripcion(descripcion, pageable);
    }
}

Resumen

Spring Data JPA simplifica significativamente la implementación de paginación y ordenación. Utilizando las interfaces y clases proporcionadas, puedes manejar grandes conjuntos de datos de manera eficiente y mejorar la experiencia del usuario en tu aplicación.

Paginación y Ordenación usando Spring Data JPA

La paginación es útil cuando tenemos un gran conjunto de datos y queremos presentarlo al usuario en partes más pequeñas. Además, a menudo necesitamos ordenar esos datos por algún criterio mientras paginamos.

En este apartado veremos un ejemplo completo de cómo paginar y ordenar fácilmente usando Spring Data JPA.

1. Configuración Inicial

Primero, supongamos que tenemos una entidad Producto como nuestra clase de dominio:

@Entity 
public class Producto {
    
    @Id
    private long id;
    private String nombre;
    private double precio; 

    // constructores, getters y setters 

}

Cada una de nuestras instancias de Producto tiene un identificador único: id, su nombre y su precio asociado.

2. Creación de un Repositorio

Para acceder a nuestros productos, necesitaremos un ProductoRepository:

public interface ProductoRepository extends PagingAndSortingRepository<Producto, Integer> {
    List<Producto> findAllByPrecio(double precio, Pageable pageable);
}

Al extender PagingAndSortingRepository, obtenemos los métodos findAll(Pageable pageable) y findAll(Sort sort) para paginación y ordenación.

También podríamos haber elegido extender JpaRepository, ya que también extiende PagingAndSortingRepository.

Una vez que extendemos PagingAndSortingRepository, podemos agregar nuestros propios métodos que toman Pageable y Sort como parámetros, como hicimos aquí con findAllByPrecio.

Veamos cómo paginar productos usando nuestro nuevo método.

3. Paginación

Una vez que tenemos nuestro repositorio extendido de PagingAndSortingRepository, solo necesitamos:

  1. Crear u obtener un objeto PageRequest, que es una implementación de la interfaz Pageable.
  2. Pasar el objeto PageRequest como argumento al método del repositorio que queremos usar.

Podemos crear un objeto PageRequest pasando el número de página solicitado y el tamaño de la página.

Aquí, la cuenta de páginas comienza en cero:

Pageable primeraPaginaConDosElementos = PageRequest.of(0, 2);
Pageable segundaPaginaConCincoElementos = PageRequest.of(1, 5);

En Spring MVC, también podemos optar por obtener la instancia de Pageable en nuestro controlador usando el soporte web de Spring Data.

Una vez que tenemos nuestro objeto PageRequest, podemos pasarlo al invocar el método de nuestro repositorio:

Page<Producto> allProductos = productoRepository.findAll(primeraPaginaConDosElementos);

List<Producto> allTenDollarProductos = productoRepository.findAllByPrecio(10, segundaPaginaConCincoElementos);

El método findAll(Pageable pageable) devuelve por defecto un objeto Page<T>.

Sin embargo, podemos optar por devolver un Page<T>, un Slice<T> o un List<T> desde cualquiera de nuestros métodos personalizados que devuelven datos paginados.

Una instancia de Page<T>, además de tener la lista de productos, también conoce el número total de páginas disponibles. Para lograr esto, desencadena una consulta de conteo adicional. Para evitar dicho costo adicional, podemos devolver un Slice<T> o un List<T> en su lugar.

Un Slice solo sabe si la siguiente porción está disponible o no.

4. Paginación y Ordenación

De manera similar, para solo tener nuestros resultados de consulta ordenados, podemos simplemente pasar una instancia de Sort al método:

Page<Producto> allProductosSortedByNombre = productoRepository.findAll(Sort.by("nombre"));

Sin embargo, ¿qué pasa si queremos tanto ordenar como paginar nuestros datos?

Podemos hacer eso pasando los detalles de ordenación en nuestro objeto PageRequest:

Pageable sortedByNombre = PageRequest.of(0, 3, Sort.by("nombre"));
Pageable sortedByPrecioDesc = PageRequest.of(0, 3, Sort.by("precio").descending());
Pageable sortedByPrecioDescNombreAsc = PageRequest.of(0, 5, Sort.by("precio").descending().and(Sort.by("nombre")));

Basándonos en nuestros requisitos de ordenación, podemos especificar los campos de ordenación y la dirección de ordenación al crear nuestra instancia de PageRequest.

Esto cubre cómo puedes utilizar la paginación y la ordenación en Spring Data JPA para manejar grandes conjuntos de datos de manera eficiente y mejorar la experiencia del usuario en tu aplicación.

06. Paginación y Ordenación usando Spring Data REST.

1. Paginación y Ordenación de Spring Data Repository en Spring Data REST

Esta sección documenta el uso de las abstracciones de paginación y ordenación de Spring Data Repository en Spring Data REST.

1.1 Paginación

En lugar de devolver todo desde un conjunto de resultados grande, Spring Data REST reconoce algunos parámetros de URL que influyen en el tamaño de la página y el número de página inicial.

Si heredas de PagingAndSortingRepository<T, ID> y accedes a la lista de todas las entidades, obtienes enlaces a las primeras 20 entidades. Para establecer el tamaño de la página en cualquier otro número, añade un parámetro size:

http://localhost:8080/gente/?size=5

En el ejemplo anterior se establece el tamaño de la página en 5.

Para usar la paginación en métodos de consulta personalizados, los que has declarado, se necesita cambiar la firma del método para aceptar un parámetro adicional Pageable y devolver una Page o Slice en lugar de una List.

Por ejemplo, el siguiente método de consulta se exporta a /gente/search/nomeStartsWith y admite paginación:

@RestResource(path = "nomeStartsWith", rel = "nomeStartsWith")
public Page findByNomeStartsWith(@Param("nome") String nome, Pageable p);

El exportador de Spring Data REST reconoce la Page/Slice devuelta y te da los resultados en el cuerpo de la respuesta, tal como lo haría con una respuesta no paginada, pero se añaden enlaces adicionales al recurso para representar las páginas anteriores y siguientes de datos.

1.3. Enlaces Anterior y Siguiente

Cada respuesta paginada devuelve enlaces a las páginas anteriores y siguientes de resultados basados en la página actual utilizando las relaciones de enlace definidas por IANA prev y next. Sin embargo, si estás en la primera página de resultados, no se renderiza el enlace prev. Para la última página de resultados, no se renderiza el enlace next.

Considera el siguiente ejemplo, donde establecemos el tamaño de la página en 5:

curl localhost:8080/gente?size=5
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente{&sort,page,size}", 
      "templated" : true
    },
    "next" : {
      "href" : "http://localhost:8080/gente?page=1&size=5{&sort}", 
      "templated" : true
    }
  },
  "_embedded" : {
     data 
  },
  "page" : { 
    "size" : 5,
    "totalElements" : 50,
    "totalPages" : 10,
    "number" : 0
  }
}

En la parte superior, vemos _links:

En la parte inferior, hay datos adicionales sobre la configuración de la página, incluido el tamaño de la página, el total de elementos, el total de páginas y el número de página que estás viendo actualmente.

Al usar herramientas como curl en la línea de comandos, si tienes un ampersand (&) en tu declaración, necesitas envolver toda la URI entre comillas.

Ten en cuenta que las URIs self y next son, de hecho, plantillas de URI. Aceptan no solo size, sino también page y sort como banderas opcionales.

Como se mencionó anteriormente, la parte inferior del documento HAL incluye una colección de detalles sobre la página. Esta información adicional facilita la configuración de herramientas de interfaz de usuario como deslizadores o indicadores para reflejar la posición general del usuario cuando visualiza los datos. Por ejemplo, el documento en el ejemplo anterior muestra que estamos viendo la primera página (con números de página que comienzan en 0).

El siguiente ejemplo muestra qué sucede cuando seguimos el enlace next:

curl "http://localhost:8080/gente?page=1&size=5"
{
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/gente{&sort,projection,page,size}",
      "templated" : true
    },
    "next" : {
      "href" : "http://localhost:8080/gente?page=2&size=5{&sort,projection}", 
      "templated" : true
    },
    "prev" : {
      "href" : "http://localhost:8080/gente?page=0&size=5{&sort,projection}", 
      "templated" : true
    }
  },
  "_embedded" : {
    "_comment": "... datos ..."
  },
  "page" : {
    "size" : 5,
    "totalElements" : 50,
    "totalPages" : 10,
    "number" : 1 
  }
}

Esto se ve muy similar, excepto por las siguientes diferencias:

Esta característica te permite mapear botones opcionales en la pantalla a estos controles de hipermedia, permitiéndote implementar características de navegación para la experiencia de usuario sin tener que codificar las URIs. De hecho, el usuario puede elegir de una lista de tamaños de página, cambiando dinámicamente el contenido servido, sin tener que reescribir los controles next y prev en la parte superior o inferior.

1.2. Ordenación

Spring Data REST reconoce los parámetros de ordenación que usan el soporte de ordenación del repositorio.

Para que tus resultados se ordenen en una propiedad particular, agrega un parámetro de URL sort con el nombre de la propiedad en la que deseas ordenar los resultados. Puedes controlar la dirección de la ordenación añadiendo una coma (,) al nombre de la propiedad más asc o desc. El siguiente ejemplo usaría el método de consulta findByNomeStartsWith definido en el PersonaRepository para todas las entidades Person con nombres que comienzan con la letra “K” y añadiría datos de ordenación que ordenan los resultados en la propiedad name en orden descendente:

curl -v "http://localhost:8080/gente/search/nomeStartsWith?name=K&sort=name,desc"

Para ordenar los resultados por más de una propiedad, sigue añadiendo tantos parámetros sort=PROPERTY como necesites. Se añaden al Pageable en el orden en que aparecen en la cadena de consulta. Los resultados pueden ordenarse por propiedades de nivel superior y anidadas. Usa la notación de ruta de propiedades para expresar una propiedad de ordenación anidada. La ordenación por asociaciones vinculables (es decir, enlaces a recursos de nivel superior) no es compatible.

2. Ejemplo de paginación REST en Spring Data

En Spring Data, si necesitamos devolver algunos resultados del conjunto de datos completo, podemos usar cualquier método de repositorio de Pageable, ya que siempre devolverá una Page. Los resultados se devolverán según el número de página, el tamaño de la página y la dirección de ordenación.

Spring Data REST reconoce automáticamente parámetros de URL como page, size, sort, etc.

Para usar los métodos de paginación de cualquier repositorio, necesitamos heredar PagingAndSortingRepository:

public interface AsuntoRepository extends PagingAndSortingRepository<Asunto, Long>{}

Si llamamos a http://localhost:8080/asuntos, Spring agrega automáticamente las sugerencias de parámetros de page, size y sort con la API:

"_links" : {
  "self" : {
    "href" : "http://localhost:8080/asuntos{?page,size,sort}",
    "templated" : true
  }
}

Por defecto, el tamaño de la página es 20, pero podemos cambiarlo llamando a algo como http://localhost:8080/asuntos?size=10.

Si queremos implementar la paginación en nuestra propia API de repositorio personalizada, necesitamos pasar un parámetro adicional Pageable y asegurarnos de que la API devuelva una Page:

@RestResource(path = "nombreContains")
public Page<Asunto> findByNombreContaining(@Param("nombre") String nombre, Pageable p);

Cada vez que agregamos una API personalizada, se agrega un endpoint /search a los enlaces generados. Entonces, si llamamos a http://localhost:8080/asuntos/search, veremos un endpoint capaz de paginación:

"findByNameContaining" : {
  "href" : "http://localhost:8080/asuntos/search/nombreContains{?nombre,page,size,sort}",
  "templated" : true
}

Todas las APIs que implementan PagingAndSortingRepository devolverán una Page. Si necesitamos devolver la lista de resultados de la Page, la API getContent() de Page proporciona la lista de registros obtenidos como resultado de la API de Spring Data REST.

10. Convertir una Lista en una Página

Supongamos que tenemos un objeto Pageable como entrada, pero la información que necesitamos recuperar está contenida en una lista en lugar de un PagingAndSortingRepository. En estos casos, es posible que necesitemos convertir una List en una Page.

Por ejemplo, imagina que tenemos una lista de resultados de un servicio SOAP:

List<Foo> list = getListOfFooFromSoapService();

Necesitamos acceder a la lista en las posiciones específicas especificadas por el objeto Pageable que se nos envía. Así que definamos el índice de inicio:

int start = (int) pageable.getOffset();

Y el índice final:

int end = (int) ((start + pageable.getPageSize()) > fooList.size() ? fooList.size()
  : (start + pageable.getPageSize()));

Teniendo estos dos en su lugar, podemos crear una Page para obtener la lista de elementos entre ellos:

Page<Foo> page 
  = new PageImpl<Foo>(fooList.subList(start, end), pageable, fooList.size());

Ahora podemos devolver page como un resultado válido.

Y ten en cuenta que si también queremos soportar la ordenación, necesitamos ordenar la lista antes de crear la sublista.


3. Paginación REST en Spring avanzada

3.1. Descripción general

Este apartado veremos cómo implementar la paginación en una API REST utilizando Spring MVC y Spring Data.

3.2. Página como Recurso vs Página como Representación

La primera pregunta al diseñar la paginación en el contexto de una arquitectura RESTful es si considerar la página como un recurso real o simplemente como una representación de los recursos.

Tratar la página en sí misma como un recurso introduce una serie de problemas, como la imposibilidad de identificar recursos de manera única entre llamadas. Esto, junto con el hecho de que, en la capa de persistencia, la página no es una entidad adecuada sino un contenedor que se construye cuando es necesario, hace que la elección sea clara; la página es parte de la representación.

La siguiente pregunta en el diseño de la paginación en el contexto de REST es dónde incluir la información de paginación:

Teniendo en cuenta que una página no es un recurso, codificar la información de la página en la URI no es una opción.

Veremos la forma estándar de resolver este problema codificando la información de paginación en una consulta URI.

3.3. El Controlador

MVC vs REST

Cuando no empleamos un marco de trabajo como Spring Data REST, la paginación se convierte en una tarea manual. En este caso, necesitamos implementar la paginación en la capa de servicio y devolver una lista paginada al controlador. Spring Data Rest proporciona una forma más sencilla de implementar la paginación, ya que la paginación se maneja automáticamente.

Ahora, para la implementación. El controlador de Spring MVC para la paginación es relativamente sencillo:

@GetMapping(params = { "page", "size" })
public List<Foo> findPaginated(@RequestParam("page") int page, @RequestParam("size") int size, UriComponentsBuilder uriBuilder, HttpServletResponse response) {
    Page<Foo> paginaResultado = service.findPaginated(page, size);
    if (page > paginaResultado.getTotalPages()) {
        throw new MyResourceNotFoundException();
    }
    eventPublisher.publishEvent(new PaginatedResultsRetrievedEvent<Foo>(
      Foo.class, uriBuilder, response, page, paginaResultado.getTotalPages(), size));

    return paginaResultado.getContent();
}

En este ejemplo, estamos inyectando los dos parámetros de consulta, size y page, en el método del controlador a través de @RequestParam.

Alternativamente, podríamos haber usado un objeto Pageable, que mapea automáticamente los parámetros de página, tamaño y orden. Además, la entidad PagingAndSortingRepository proporciona métodos inmediatos para usar que admiten el uso de Pageable como parámetro.

También estamos inyectando la respuesta HTTP y el UriComponentsBuilder para ayudar con la descubribilidad, que estamos desacoplando mediante un evento personalizado. Si eso no es un objetivo de la API, simplemente podemos eliminar el evento personalizado.

Finalmente, nota que el enfoque de este apartado es solo la capa REST y web; para profundizar en la parte de acceso a datos de la paginación, podemos consultar los apartados anteriores con Spring Data.

3.4. Cómo descubrir la Paginación REST

Dentro del alcance de la paginación, satisfacer la restricción HATEOAS de REST significa permitir que el cliente de la API descubra las páginas siguiente y anterior basándose en la página actual en la navegación. Para este propósito, utilizaremos el encabezado HTTP Link, junto con los tipos de relación de enlace “next”, “prev”, “first” y “last”.

En REST, la “descubribilidad” es una preocupación transversal, aplicable no solo a operaciones específicas, sino a tipos de operaciones. Por ejemplo, cada vez que se crea un recurso, el URI de ese recurso debería ser descubrible por el cliente. Dado que este requisito es relevante para la creación de CUALQUIER recurso, lo manejaremos por separado.

Desacoplaremos estas preocupaciones usando eventos, como discutimos en el artículo anterior centrado en la descubribilidad de un servicio REST. En el caso de la paginación, el evento, PaginatedResultsRetrievedEvent, se dispara en la capa del controlador. Luego implementaremos la descubribilidad con un oyente personalizado para este evento.

En resumen, el oyente comprobará si la navegación permite páginas siguiente, anterior, primera y última. Si lo hace, agregará las URIs relevantes a la respuesta como un encabezado HTTP ‘Link’.

Ahora vamos paso a paso. El UriComponentsBuilder pasado desde el controlador contiene solo la URL base (el host, el puerto y la ruta de contexto). Por lo tanto, tendremos que agregar las secciones restantes:

void addLinkHeaderOnPagedResourceRetrieval( UriComponentsBuilder uriBuilder, HttpServletResponse response, Class clazz, int page, int totalPages, int size ){

   String resourceName = clazz.getSimpleName().toString().toLowerCase();
   uriBuilder.path( "/admin/" + resourceName );

    // ...
}

A continuación, utilizaremos un StringJoiner para concatenar cada enlace. Usaremos el uriBuilder para generar las URIs. Veamos cómo procedemos con el enlace a la siguiente página:

StringJoiner linkHeader = new StringJoiner(", ");
if (hasNextPage(page, totalPages)){
    String uriForNextPage = constructNextPageUri(uriBuilder, page, size);
    linkHeader.add(createLinkHeader(uriForNextPage, "next"));
}

Veamos la lógica del método constructNextPageUri:

String constructNextPageUri(UriComponentsBuilder uriBuilder, int page, int size) {
    return uriBuilder.replaceQueryParam(PAGE, page + 1)
      .replaceQueryParam("size", size)
      .build()
      .encode()
      .toUriString();
}

Procederemos de manera similar para el resto de las URIs que queremos incluir.

Finalmente, agregaremos la salida como un encabezado de respuesta:

response.addHeader("Link", linkHeader.toString());

Nota que, por brevedad, solo se incluye una muestra parcial del código y el código completo está aquí.

3.5. Pruebas de la Paginación

Tanto la lógica principal de la paginación como la descubribilidad están cubiertas por pruebas de integración pequeñas y enfocadas. Como en el artículo anterior, utilizaremos la biblioteca REST-assured para consumir el servicio REST y verificar los resultados.

Estos son algunos ejemplos de pruebas de integración de paginación; para una suite de pruebas completa, consulta el proyecto en GitHub (enlace al final del artículo):

@Test
public void whenResourcesAreRetrievedPaged_then200IsReceived(){
    Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

    assertThat(response.getStatusCode(), is(200));
}
@Test
public void whenPageOfResourcesAreRetrievedOutOfBounds_then404IsReceived(){
    String url = getFooURL() + "?page=" + randomNumeric(5) + "&size=2";
    Response response = RestAssured.get.get(url);

    assertThat(response.getStatusCode(), is(404));
}
@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources(){
   createResource();
   Response response = RestAssured.get(paths.getFooURL() + "?page=0&size=2");

   assertFalse(response.body().as(List.class).isEmpty());
}

3.6. Pruebas de la Descubribilidad de la Paginación

Probar que la paginación es descubrible por un cliente es relativamente sencillo, aunque hay mucho terreno por cubrir.

Las pruebas se centrarán en la posición de la página actual en la navegación y las diferentes URIs que deberían ser descubribles desde cada posición:

@Test
public void whenFirstPageOfResourcesAreRetrieved_thenSecondPageIsNext(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertEquals(getFooURL()+"?page=1&size=2", uriToNextPage);
}
@Test
public void whenFirstPageOfResourcesAreRetrieved_thenNoPreviousPage(){
   Response response = RestAssured.get(getFooURL()+"?page=0&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertNull(uriToPrevPage );
}
@Test
public void whenSecondPageOfResourcesAreRetrieved_thenFirstPageIsPrevious(){
   Response response = RestAssured.get(getFooURL()+"?page=1&size=2");

   String uriToPrevPage = extractURIByRel(response.getHeader("Link"), "prev");
   assertEquals(getFooURL()+"?page=0&size=2", uriToPrevPage);
}
@Test
public void whenLastPageOfResourcesIsRetrieved_thenNoNextPageIsDiscoverable(){
   Response first = RestAssured.get(getFooURL()+"?page=0&size=2");
   String uriToLastPage = extractURIByRel(first.getHeader("Link"), "last");

   Response response = RestAssured.get(uriToLastPage);

   String uriToNextPage = extractURIByRel(response.getHeader("Link"), "next");
   assertNull(uriToNextPage);
}

Nota que el código completo de bajo nivel para extractURIByRel, responsable de extraer las URIs por relación rel, está aquí.

3.7. Obtener Todos los Recursos

Sobre el mismo tema de paginación y descubribilidad, se debe tomar la decisión de

si se permite al cliente recuperar todos los recursos del sistema de una vez, o si el cliente debe solicitarlos paginados.

Si se decide que el cliente no puede recuperar todos los recursos con una sola solicitud, y se requiere paginación, entonces varias opciones están disponibles para la respuesta de una solicitud. Una opción es devolver un 404 (Not Found) y usar el encabezado Link para hacer que la primera página sea descubrible:

Link=<http://localhost:8080/rest/api/admin/foo?page=0&size=2>; rel=”first”, <http://localhost:8080/rest/api/admin/foo?page=103&size=2>; rel=”last”

Otra opción es devolver una redirección, 303 (See Other), a la primera página. Una ruta más conservadora sería simplemente devolver al cliente un 405 (Method Not Allowed) para la solicitud GET.

3.8. Paginación REST con Encabezados HTTP Range

Una forma relativamente diferente de implementar la paginación es trabajar con los encabezados HTTP Range: Range, Content-Range, If-Range, Accept-Ranges, y códigos de estado HTTP, 206 (Partial Content), 413 (Request Entity Too Large) y 416 (Requested Range Not Satisfiable).

Una visión de este enfoque es que las extensiones del rango HTTP no están destinadas para la paginación y deben ser manejadas por el servidor, no por la aplicación. Implementar la paginación basada en las extensiones del encabezado HTTP Range es técnicamente posible, aunque no es tan común como la implementación discutida en este artículo.

3.9. Conclusión

Este apartado ilustró cómo implementar la paginación en una API REST utilizando Spring y discutió cómo configurar y probar la descubribilidad.

07. Spring Data JPA con Vaadin (práctica).

2. Creación de una interfaz de usuario (Web) CRUD con Vaadin

En esta práctica construiremos una aplicación que utiliza una interfaz de usuario (UI) basada en Vaadin en Frontend y Spring Data JPA en el Backend.

El objetivo es construir una interfaz de usuario Vaadin para un repositorio JPA, realizando una aplicación Web con funcionalidad completa de CRUD (Create, Read, Update, Delete) y un ejemplo de filtrado que utiliza un método personalizado del repositorio.

2.1. Requisitos

Como hemos visto, también es posible emplear Spring Initializr para generar el proyecto con el IDE:

2.2. Configuración del proyecto: Spring Initializr

  1. Accede a https://start.spring.io. Permite añadir todas las dependencias necesarias para una aplicación y realiza la mayoría de la configuración.
  2. Elige Maven y Java.
  3. Haz clic en “Dependencies” y selecciona Vaadin, Spring Data JPA y H2 Database.
  4. Haz clic en “Generate”.
  5. Descarga el archivo ZIP resultante, que es un archivo comprimido de una aplicación web configurada con tus para trabajar con Vaadin, JPA y H" como sistema gestor de base de datos.

Si el IDE (Visual Studio Code, Eclipse o Intellj Ultimate) tiene integración con Spring Initializr, puedes hacer este proceso desde el IDE.

Spring Initializr con Vaadin Spring Initializr con Vaadin

2.3. Servicios de Backend (entidades y repositorios)

En este punto necesitamos crear las clases para los objetos de entidad y repositorio.

La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/Usuario.java) define la entidad del Usuario:

package local.sanclemente.ad.crudvaadin;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Usuario {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idUsuario;

    private String nombre;
    private String apellidos;

    protected Usuario() {}

    public Usuario(String nombre, String apellidos) {
        this.nombre = nombre;
        this.apellidos = apellidos;
    }

    // Getters, setters, y toString...
}

La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/CustomerRepository.java) define el repositorio del usuario:

package local.sanclemente.ad.crudvaadin;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface RepositorioUsuario extends JpaRepository<Usuario, Long> {
    List<Usuario> findByApellidosStartsWithIgnoreCase(String apellidos);
}

La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/CrudvaadinApplication.java) muestra la clase de la aplicación, que crea algunos datos para ti:

package local.sanclemente.ad.crudvaadin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class CrudvaadinApplication {

    private static final Logger log = LoggerFactory.getLogger(CrudvaadinApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(CrudvaadinApplication.class);
    }

    /**
     * Este método se ejecuta al inicio de la aplicación y carga algunos datos de ejemplo.
     * @param repository
     * @return CommandLineRunner
     */
    @Bean // Esta anotación le dice a Spring que ejecute este método al inicio
    public CommandLineRunner loadData(RepositorioUsuario repository) {
        return (args) -> {
            // creamos algunos usuarios con nombres de poetas y apellidos de personajes de sus obras
            repository.save(new Usuario("Federico", "Lorca"));
            repository.save(new Usuario("Antonio", "Machado"));
            repository.save(new Usuario("Miguel", "Hernández"));
            repository.save(new Usuario("Gustavo", "Adolfo Bécquer"));
            repository.save(new Usuario("Luis", "Cernuda"));
            repository.save(new Usuario("Rafael", "Alberti"));
            // Poetas norteamericanas del género de poería confesional:
            repository.save(new Usuario("Sylvia", "Plath"));
            repository.save(new Usuario("Anne", "Sexton"));
            repository.save(new Usuario("Sharon", "Olds"));
            repository.save(new Usuario("Louise", "Glück"));
            repository.save(new Usuario("Lucie", "Brock-Broido"));
            repository.save(new Usuario("Jorie", "Graham"));
            

            // REcoge todos los usuarios y los muestra en el log
            log.info("Poetas/poetisas encontrados/as con findAll():");
            log

.info("-------------------------------");
            for (Usuario usuario : repository.findAll()) {
                log.info(usuario.toString());
            }
            log.info("");

            // obtención de usuario por ID
            Usuario usuario = repository.findById(1L).get();
            log.info("Usuario con findOne(1L):");
            log.info("--------------------------------");
            log.info(usuario.toString());
            log.info("");

            // fetch customers by last name
            log.info("Usuario encontrado con findByApellidosStartsWithIgnoreCase('Plath'):");
            log.info("--------------------------------------------");
            for (Usuario plath : repository.findByApellidosStartsWithIgnoreCase("Plath")) {
                log.info(plath.toString());
            }
            log.info("");
        };
    }
}

CommadLineRunner es una interfaz funcional que se ejecuta al inicio de la aplicación. En este caso, se utiliza para cargar algunos datos de ejemplo en la base de datos. Spring boor llama automáticamente a todos los beans CommandLineRunner una vez que el contexto de la aplicación está cargado y ejecuta el método run de cada bean.

Suele emplearse para realizar tareas de inicialización, como cargar datos de ejemplo en la base de datos. Se puede:

Ejemplos: Spring Boot CommandLineRunner Example

2.4. La vista: dependencias de Vaadin

Las dependencias ya esá configuradas. Sin embargo, en este apartado se describe cómo agregar el soporte de Vaadin a un proyecto Spring nuevo.

La integración de Vaadin en Spring contiene una colección de dependencias de inicio de Spring Boot, así que solo necesitas agregar el siguiente fragmento de Maven (o una configuración de Gradle correspondiente), que ya ha sido añadido por Spring Initializr:

<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

El ejemplo utiliza una versión más nueva de Vaadin que la versión predeterminada traída por el módulo de inicio (Spring Initializr). Para usar una versión más reciente, define el Bill of Materials (BOM) de Vaadin de la siguiente manera:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

En el modo de desarrollo, la dependencia es suficiente, mas cuando estás construyendo para producción, necesitas habilitar tu aplicación para compilaciones de producción.

Por defecto, Gradle no admite los BOM, pero hay un práctico complemento para eso. Consulta el archivo de construcción build.gradle para ver un ejemplo de cómo lograr lo mismo.

2.4. La vista: clase principal de la vista de Vaadin

La clase principal de la vista (que hemos llamado MainView) es el punto de entrada para la lógica de la interfaz de usuario de Vaadin. En las aplicaciones Spring Boot, si la anotas con @Route, se recoge automáticamente y se muestra en la raíz de tu aplicación web.

Se puede personalizar la URL donde se muestra la vista al dar un parámetro a la anotación @Route. La siguiente lista (del proyecto inicial en src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra una vista simple de saludo:

package local.sanclemente.ad.crudvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;

@Route
public class MainView extends VerticalLayout {

    public MainView() {
        add(new Button("Saluda", e -> Notification.show("¡Hola, estudiante de Acceso a Datos!")));
    }
}

2.5. Listado de usuarios en un Grid de Datos

Existen muchos componentes visuales Vaadin: https://vaadin.com/docs/latest/components, uno de ellos es el Grid, que es un componente de tabla de datos que muestra una lista de objetos en una tabla.

Para un diseño listado, puedes usar el componente Grid. Puedes pasar la lista de entidades desde un RepositorioUsuario inyectado por constructor al Grid usando el método setItems. El cuerpo de la clase MainView sería así:

@Route
public class MainView extends VerticalLayout {

    private final RepositorioUsuario repo;
    final Grid<Usuario> grid;

    public MainView(RepositorioUsuario repo) {
        this.repo = repo;
        this.grid = new Grid<>(Usuario.class);
        add(grid);
        listarUsuarios();
    }

    private void listarUsuarios() {
        // El método `findAll` es implícito en el RepositorioUsuario e implementado por Spring Data JPA
        grid.setItems(repo.findAll()); // Carga todos los usuarios
    }
}
Nota

Si disponemos de tablas grandes o muchos usuarios concurrentes, es mejor no vincular todo el conjunto de datos a tus componentes de UI.

Aunque Vaadin Grid carga perezosamente los datos del servidor al navegador, el enfoque anterior mantiene toda la lista de datos en la memoria del servidor. Para ahorrar algo de memoria, e pueden mostrar solo los resultados más importantes mediante la paginación o el uso de carga perezosa, por ejemplo, utilizando el método:

grid.setItems(VaadinSpringDataHelpers.fromPagingRepository(repo)).

2.6. Filtrado de datos

Se puede usar un componente TextField para crear una entrada de filtro. Para hacerlo, primero modifica el método listarUsuarios() para admitir el filtrado. El siguiente ejemplo (en src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra cómo hacerlo:

void listarUsuarios(String textoFiltro) {
    if (StringUtils.hasText(textoFiltro)) {
        grid.setItems(repo.findByApellidosStartsWithIgnoreCase(textoFiltro));
    } else {
        grid.setItems(repo.findAll());
    }
}
Aquí es donde son útiles las **consultas declarativas de Spring Data**. **Escribir `findByApellidosStartsWithIgnoringCase` es una definición de una sola línea en la interfaz RepositorioUsuario**.

Puedes añadir un listener al componente TextField y pasar su valor a ese método de filtro. El ValueChangeListener se llama automáticamente a medida que un usuario escribe porque defines el ValueChangeMode.LAZY en el campo de texto del filtro. El siguiente ejemplo muestra cómo configurar dicho listener:

TextField filtro = new TextField();
filtro.setPlaceholder("Filtra por apellido");
filtro.setValueChangeMode(ValueChangeMode.LAZY);
filtro.addValueChangeListener(e -> listarUsuarios(e.getValue()));
add(filtro, grid);

2.7. Definición de un Editor de Usuarios personalizado

Como las UI de Vaadin son código Java, se puede escribir código reutilizable desde el principio.

Para hacerlo, se puede definir un componente editor para tu entidad Usuario. Puedes hacer que sea un bean administrado por Spring para que puedas inyectar directamente el RepositorioUsuario en el editor y abordar las partes de Crear, Actualizar y Eliminar de tu funcionalidad CRUD. El siguiente ejemplo (del archivo src/main/java/local/sanclemente/ad/crudvaadin/EditorUsuario.java) muestra cómo hacerlo:

package local.sanclemente.ad.crudvaadin;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Un ejemplo sencillo para introducir la construcción de formularios. Dado que una aplicación real probablemente sea mucho más complicada que este ejemplo, podrías reutilizar este formulario en varios sitios. Este
 * componente de ejemplo se utiliza únicamente en la vista principal, MainView.
 * <p>
 * En una aplicación del mundo real, es probable que utilices una superclase común para todos tus
 * formularios: menos código, mejor experiencia de usuario.
 */
@SpringComponent
@UIScope
public class EditorUsuario extends VerticalLayout implements KeyNotifier {

    private final RepositorioUsuario repository;

    /**
     * Usuario que estamos editando
     */
    private Usuario usuario;

    /* Para editar las propiedades de la entidad Usuario */
    TextField nombre = new TextField("Nombre");
    TextField apellidos = new TextField("Apellidos");

    /* Botones de acción */
    Button guardar = new Button("Guardar", VaadinIcon.CHECK.create());
    Button cancel = new Button("Cancelar");
    Button delete = new Button("Borrar", VaadinIcon.TRASH.create());
    HorizontalLayout accionesLayout = new HorizontalLayout(guardar, cancel, delete);

    // Binder para enlazar propiedades y campos
    Binder<Usuario> binder = new Binder<>(Usuario.class);
    private ChangeHandler changeHandler;

    @Autowired
    public EditorUsuario(RepositorioUsuario repository) {
        this.repository = repository;

        add(nombre, apellidos, accionesLayout);

        // enlace de los campos con las propiedades de la entidad Usuario
        binder.bindInstanceFields(this);

        // Configuración y estilos de los botones
        setSpacing(true);

        guardar.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        delete.addThemeVariants(ButtonVariant.LUMO_ERROR);

        addKeyPressListener(Key.ENTER, e -> guardar());

        // Escucha cambios realizados por el editor, actualiza la lista de usuarios
        guardar.addClickListener(e -> guardar());
        delete.addClickListener(e -> delete());
        cancel.addClickListener(e -> editUsuario(usuario));
        setVisible(false);
    }

    void delete() {
        repository.delete(usuario);
        changeHandler.onChange();
    }

    void guardar() {
        repository.save(usuario);
        changeHandler.onChange();
    }

    public interface ChangeHandler {
        void onChange();
    }

    public final void editUsuario(Usuario c) {
        if (c == null) {
            setVisible(false);
            return;
        }
        final boolean existePersistido = c.getIdUsuario() != null;
        if (existePersistido) {
            // Busca la entidad actualizada con el mismo ID
            // En una aplicación del mundo real, esto debería comprobar si realmente existe en la base de datos
            // Con carga perezosa para las relaciones con la entidad.
            usuario = repository.findById(c.getIdUsuario()).get();
        }
        else {
            usuario = c;
        }
        cancel.setVisible(existePersistido);

        // Enlaza las propiedades del usuario con los nombres similares de los nombres de los campos.
        // Podría usaser una anotación o "manual binding" o por medio de programación
        // moviendo los valores de los campos a la entidad y viceversa.
        binder.setBean(usuario);

        setVisible(true);

        // Enfoca el nombre inicialmente
        nombre.focus();
    }

    public void setChangeHandler(ChangeHandler h) {
        // ChangeHandler es notificado cuando guardas o borras haciendo clic.
        changeHandler = h;
    }

}

Gestión de eventos con Vaadin: https://vaadin.com/docs/latest/create-ui/creating-components/events

En una aplicación más grande, podrías usar este componente editor en varios lugares. También ten en cuenta que, en aplicaciones grandes, es posible que desees aplicar algunos patrones comunes (como MVP) para estructurar tu código de UI.

2.8. Conectar el Editor de manera bidireccional

En los pasos anteriores, ya has visto algunos conceptos básicos de la programación basada en componentes. Al usar un botón y agregar un listener de selección a la cuadrícula, puedes integrar completamente tu editor en la vista principal. La siguiente lista (del archivo src/main/java/local/sanclemente/ad/crudvaadin/MainView.java) muestra la versión final de la clase MainView:

package local.sanclemente.ad.crudvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import org.springframework.util.StringUtils;

@Route
public class MainView extends VerticalLayout {

    private final RepositorioUsuario repo;
    private final EditorUsuario editor;

    final Grid<Usuario> grid;

    final TextField filtro;

    private final Button addNewBtn;

    public MainView(RepositorioUsuario repo, EditorUsuario editor) {
        this.repo = repo;
        this.editor = editor;
        this.grid = new Grid<>(Usuario.class);
        this.filtro = new TextField();
        this.addNewBtn = new Button("Nuevo usuario", VaadinIcon.PLUS.create());

        // Disposición de los componentes:
        HorizontalLayout accionesLayout = new HorizontalLayout(filtro, addNewBtn);
        add(accionesLayout, grid, editor);

        grid.setHeight("300px");
        grid.setColumns("idUsuario", "nombre", "apellidos");
        grid.getColumnByKey("idUsuario").setWidth("50px").setFlexGrow(0);

        filtro.setPlaceholder("Filtrar por apellidos");

        // Enlace de la lógica con los componentes

        // Sustituye el listado cuando se aplica un filtro:
        filtro.setValueChangeMode(ValueChangeMode.LAZY);
        filtro.addValueChangeListener(e -> listarUsuarios(e.getValue()));

        // Conecta el Usuario seleccionado al editor o lo oculta si no está seleccionado
        grid.asSingleSelect().addValueChangeListener(e -> {
            editor.editUsuario(e.getValue());
        });

        // Instancia y edita un nuevo Usuario cuando se pulsa el botón "Nuevo Usuario"
        addNewBtn.addClickListener(e -> editor.editUsuario(new Usuario("", "")));

        // Escucha los cambios hechos por el editor, refresca los datos del modelo.
        editor.setChangeHandler(() -> {
            editor.setVisible(false);
            listarUsuarios(filtro.getValue());
        });

        // Inicializa el listado
        listarUsuarios(null);
    }

    // tag::listarUsuarios[]
    void listarUsuarios(String textoFiltro) {
        if (StringUtils.hasText(textoFiltro)) {
            grid.setItems(repo.findByApellidosStartsWithIgnoreCase(textoFiltro));
        } else {
            grid.setItems(repo.findAll());
        }
    }
    // end::listarUsuarios[]

}

2.9. Construir un JAR Ejecutable

Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven. También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias, clases y recursos necesarios y ejecutarlo.

Construir un JAR ejecutable facilita el transporte, la copia de seguridad y el intercambio con otros.

Para construir un JAR ejecutable, ejecute el siguiente comando:

./gradlew clean build

o

./mvnw clean install

Luego, puedes ejecutar el JAR con el siguiente comando:

java -jar build/libs/crudvaadin-0.0.1-SNAPSHOT.jar

o

java -jar target/crudvaadin-0.0.1-SNAPSHOT.jar

2.10. Resultados

Visita http://localhost:8080 en tu navegador web. Deberías ver la aplicación Vaadin ejecutándose con un formulario para agregar, editar, filtrar y eliminar clientes.

Ejercicio

Crea una aplicación que con la base de datos de películas, permita mostrar películas buscando por el título en castellano. Realiza una parte sólo de consulta con una versión reducida de la entidad Pelicula.

08. Ejecutores al inicio con Spring Boot

Nota: para la realización de una aplicación de consola en Spring Boot, puedes seguir el siguiente tutorial: https://www.baeldung.com/spring-boot-console-app.

Existen varias formas de hacerlo:

@SpringBootApplication
public class MyApplication implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MyApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}
@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

Ejecutores de código al inicio de la Aplicación

Para la realización de una aplicación de consola en Spring Boot, es necesario crear un proyecto de Spring Boot y modificar la clase principal de la aplicación para que sea una aplicación de consola.

package com.micompanhia.miproyecto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Existen varias formas de hacerlo:

@SpringBootApplication
public class MiAplicacion implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(String... args) {
        // Aquí va el código de la aplicación
    }
}

Ejemplo:

@SpringBootApplication
public class MiAplicacion implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Override
    public void run(ApplicationArguments args) {
        // Aquí va el código de la aplicación
    }
}
@Component
public class MiComponente {

    @PostConstruct
    public void init() {
        // Aquí va el código de la aplicación
    }
}

@Component es una anotación que marca una clase como un componente de Spring. Spring escaneará las clases anotadas con @Component y las registrará en el contexto de la aplicación.

@PostConstruct es una anotación que se utiliza en un método que debe ejecutarse después de que se haya completado la construcción de un bean. Spring ejecutará el método anotado con @PostConstruct después de que se haya creado el bean.

@SpringBootApplication
public class MiAplicacion {

    public static void main(String[] args) {
        SpringApplication.run(MiAplicacion.class, args);
    }

    @Bean
    public CommandLineRunner run() {
        return args -> {
            // Aquí va el código de la aplicación
        };
    }
}

@Bean es una anotación que marca un método como un productor de un bean administrado por Spring. Spring llamará al método anotado con @Bean para crear el bean y lo registrará en el contexto de la aplicación.

Diferencias entre ejecutores de código al inicio de la Aplicación

Estos ejecutores se utilizan para ejecutar la lógica al iniciar la aplicación:

ApplicationRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

ApplicationRunner recoge ApplicationArguments, que tiene métodos como getOptionNames(), getOptionValues() y getSourceArgs().

CommandLineRunner run() se ejecutará justo después de que se cree el ApplicationContext y antes de que inicie la aplicación Spring Boot.

Acepta los argumentos como un array de String que se pasan en el momento del inicio del servidor.

Ambos proporcionan la misma funcionalidad y la única diferencia entre CommandLineRunner y ApplicationRunner es que CommandLineRunner.run() acepta un array de String[], mientras que ApplicationRunner.run() acepta ApplicationArguments como argumento.

Puedes encontrar más información con ejemplos en la Guía para Ejecutar Lógica en el Inicio en Spring.

09. Servicios, componentes y repositorios

Hasta ahora, hemos visto de manera global Spring Boot, pero no hemos profundizado en todos sus componentes. En este apartado hablaremos de los componentes más importantes de Spring Boot, que son los servicios, componentes y repositorios, así como las diferencias entre ellos.

Los tres componentes esenciales de una aplicación Spring Boot son:

Servicios

Un servicio es un concepto fundamental en las aplicaciones Spring Boot. Representa una capa de la aplicación responsable de ejecutar la lógica de negocio y encapsular la funcionalidad de la aplicación. Generalmente, los servicios son sin estado y están diseñados para realizar tareas específicas.

@Service
public class ServicioProducto {

    @Autowired
    private RepositorioProducto repositorioProducto;

    public List<Producto> obtenerTodosLosProductos() {
        return repositorioProducto.findAll();
    }

    public Producto obtenerProductoPorId(Long id) {
        return repositorioProducto.findById(id).orElse(null);
    }

    public void guardarProducto(Producto producto) {
        repositorioProducto.save(producto);
    }

    public void eliminarProducto(Long id) {
        repositorioProducto.deleteById(id);
    }
}

En el ejemplo anterior, ServicioProducto encapsula la lógica de negocio relacionada con los productos. Interactúa con un RepositorioProducto para realizar operaciones CRUD.

Componentes

Componente es un término amplio que incluye tanto la anotación @Component como sus variantes especializadas como @Service, @Controller y @Repository. Todas estas anotaciones son especializaciones de @Component y se utilizan para definir beans de Spring.

@Component
public class EnviadorEmail {

    public void enviarEmail(String destinatario, String asunto, String cuerpo) {
        // Lógica para enviar el correo electrónico
        // ...
        System.out.println("Correo enviado a " + destinatario);
    }
}

Aquí, EnviadorEmail es un componente sencillo, encargado de enviar correos electrónicos. Puede ser inyectado en otros componentes de Spring.

Repositorios

El repositorio, que ya hemos estudiado, pero que es conveniente declarar, es un concepto en Spring que simplifica el acceso a datos. Generalmente, se utiliza para interactuar con bases de datos.

Como sabes existen muchos tipos de repositorios, pero el más interesante para nosotros ahora es Spring Data JPA, que proporciona una manera poderosa y sencilla de trabajar con datos, generando automáticamente el código repetitivo necesario.

@Repository
public interface RepositorioProducto extends JpaRepository<Producto, Long> {

    List<Producto> findByCategoria(String categoria);

    // Se pueden definir consultas personalizadas adicionales
}

RepositorioProducto extiende JpaRepository y hereda varios métodos para operaciones comunes con la base de datos. Además, se pueden definir consultas personalizadas para un acceso más flexible a los datos.


Integración de Servicios, Componentes y Repositorios

@Service
public class ServicioPedido {

    @Autowired // inyección de dependencias
    private ServicioProducto servicioProducto; // El servicio encapsula la lógica de negocio y la comunicación con el repositorio

    @Autowired
    private EnviadorEmail enviadorEmail;

    public void procesarPedido(Long idProducto, String emailUsuario) {
        Producto producto = servicioProducto.obtenerProductoPorId(idProducto);

        // Lógica de negocio para procesar el pedido
        // ...

        enviadorEmail.enviarEmail(
            emailUsuario,
            "Confirmación de Pedido",
            "¡Tu pedido ha sido procesado exitosamente!"
        );
    }
}

En este ejemplo, ServicioPedido utiliza tanto ServicioProducto como EnviadorEmail para realizar el procesamiento del pedido. Cada componente cumple un rol específico, y su colaboración da como resultado una aplicación coherente y bien estructurada.

Comprender los roles de Servicios, Componentes y Repositorios es fundamental para construir aplicaciones Spring Boot mantenibles y escalables.

10. Servicios

En este apartado vamos a profundizar en qué es un servicio en el contexto de Spring Boot y Spring Data JPA, sus características principales, cómo se crea, y terminaremos con un ejemplo completo y funcional que cubre desde la entidad hasta el uso del servicio.

Servicio en Spring

En Spring, un servicio es una clase de la capa de negocio que contiene la lógica principal de la aplicación.

Actúa como un puente entre el controlador (que gestiona la entrada del usuario) y el repositorio (que gestiona el acceso a datos).

Características

Creación

  1. Crear la entidad (@Entity). Ya visto.
    • Debe tener un constructor vacío y otro con todos los atributos.
    • Debe tener un @Id.
    • Debe tener getters y setters para todos los atributos.
    • Debe tener un método toString() para facilitar la depuración.
  2. Crear un repositorio (extends JpaRepository u otro tipo de repositorio).
    • Debe estar anotado con @Repository.
    • Debe extender de JpaRepository o de otro tipo de repositorio (CrudRepository, PagingAndSortingRepository, etc.).
    • Debe contener métodos para acceder a los datos (CRUD).
    • Puede contener métodos personalizados para consultas específicas.
  3. Crear el servicio (@Service), inyectando el repositorio.
  4. Crear un controlador (@RestController) para gestionar las solicitudes HTTP.
    • Debe estar anotado con @RestController.
    • Debe contener métodos para gestionar las solicitudes HTTP (GET, POST, PUT, DELETE).
    • Debe inyectar el servicio.
  5. Usar el servicio desde un controlador o desde otro servicio.

Ejercicio

En este ejemplo vamos a crear una API REST para gestionar productos. La API tendrá las siguientes funcionalidades:

Realiza el ejercicio, siguiendo los pasos y creando los ficheros necesarios para completar la API REST.

1. Entidad Producto.java

import jakarta.persistence.*;

@Entity
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idProducto;

    private String nombre;
    private String categoria;
    private double precio;

    // Constructores
    public Producto() {}

    public Producto(String nombre, String categoria, double precio) {
        this.nombre = nombre;
        this.categoria = categoria;
        this.precio = precio;
    }

    // Getters y Setters
    public Long getIdProducto() { return idProducto; }
    public void setIdProducto(Long idProducto) { this.idProducto = idProducto; }

    public String getNombre() { return nombre; }
    public void setNombre(String nombre) { this.nombre = nombre; }

    public String getCategoria() { return categoria; }
    public void setCategoria(String categoria) { this.categoria = categoria; }

    public double getPrecio() { return precio; }
    public void setPrecio(double precio) { this.precio = precio; }
    
    // Resto de métodos: toString, equals, hashCode
}

2. Repositorio ProductoRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface ProductoRepository extends JpaRepository<Producto, Long> {
    List<Producto> findByCategoria(String categoria);
}

3. Servicio ProductoService.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;

@Service
public class ProductoService {

    private final ProductoRepository productoRepository;

    @Autowired
    public ProductoService(ProductoRepository productoRepository) {
        this.productoRepository = productoRepository;
    }

    public List<Producto> obtenerTodos() {
        return productoRepository.findAll();
    }

    public Producto obtenerPorId(Long id) {
        return productoRepository.findById(id).orElse(null);
    }

    public List<Producto> obtenerPorCategoria(String categoria) {
        return productoRepository.findByCategoria(categoria);
    }

    public Producto guardar(Producto producto) {
        return productoRepository.save(producto);
    }

    public void eliminar(Long id) {
        productoRepository.deleteById(id);
    }

    public boolean existe(Long id) {
        return productoRepository.existsById(id);
    }
}

4. Controlador ProductoController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/productos")
public class ProductoController {

    private final ProductoService productoService; // Se ha añadido una nueva capa entre el controlador y el repositorio

    @Autowired
    public ProductoController(ProductoService productoService) {
        this.productoService = productoService;
    }

    @GetMapping
    public List<Producto> obtenerTodos() {
        return productoService.obtenerTodos();
    }

    @GetMapping("/{id}")
    public Producto obtenerPorId(@PathVariable Long id) {
        return productoService.obtenerPorId(id);
    }

    @PostMapping
    public Producto crearProducto(@RequestBody Producto producto) {
        return productoService.guardar(producto);
    }

    @DeleteMapping("/{id}")
    public void eliminarProducto(@PathVariable Long id) {
        productoService.eliminar(id);
    }

    @GetMapping("/categoria/{categoria}")
    public List<Producto> buscarPorCategoria(@PathVariable String categoria) {
        return productoService.obtenerPorCategoria(categoria);
    }
}

5. Peticiones REST

Puedes probar esta API usando curl o Postman:

Configuración del proyecto

Dependencias con PostgreSQL y JPA

Si usas Spring Initializr, asegúrate de seleccionar:

Si estás usando Maven, las dependencias del archivo pom.xml serían (aproximadamente) las siguientes:

<dependencies>
    <!-- Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- PostgreSQL -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Pruebas unitarias -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Configuración application.properties

Crea este archivo en src/main/resources/application.properties:

# Configuración básica de conexión
spring.datasource.url=jdbc:postgresql://localhost:5432/tienda
spring.datasource.username=usuario
spring.datasource.password=contraseña

# Configuración de JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Añade la estrategia de nombrado para evitar problemas con mayúsculas
spring.jpa.properties.hibernate.globally_quoted_identifiers=true
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

# Puerto del servidor
server.port=8080

Asegúrate de que tienes una base de datos llamada tienda creada en PostgreSQL y un usuario con permisos.

Si aún no has creado tu base de datos, puedes hacerlo desde la terminal:

CREATE DATABASE tienda;
CREATE USER usuario WITH ENCRYPTED PASSWORD 'contraseña';
GRANT ALL PRIVILEGES ON DATABASE tienda TO usuario;

O también desde pgAdmin.

Pruebas Unitarias del Servicio con Mockito y JUnit (opcional)

Vamos a probar ProductoService sin tocar la base de datos real, usando un repositorio simulado (mock).

ProductoServiceTest.java

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.Optional;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class ProductoServiceTest {

    private ProductoRepository productoRepository;
    private ProductoService productoService;

    @BeforeEach
    void setUp() {
        productoRepository = Mockito.mock(ProductoRepository.class);
        productoService = new ProductoService(productoRepository);
    }

    @Test
    void testObtenerTodos() {
        Producto producto1 = new Producto("Teclado", "Electrónica", 49.99);
        Producto producto2 = new Producto("Ratón", "Electrónica", 19.99);
        when(productoRepository.findAll()).thenReturn(Arrays.asList(producto1, producto2));

        List<Producto> productos = productoService.obtenerTodos();

        assertEquals(2, productos.size());
        verify(productoRepository, times(1)).findAll();
    }

    @Test
    void testGuardarProducto() {
        Producto producto = new Producto("Monitor", "Electrónica", 150.00);
        when(productoRepository.save(producto)).thenReturn(producto);

        Producto guardado = productoService.guardar(producto);

        assertNotNull(guardado);
        assertEquals("Monitor", guardado.getNombre());
        verify(productoRepository).save(producto);
    }

    @Test
    void testObtenerPorId_Existe() {
        Producto producto = new Producto("Tablet", "Electrónica", 200.00);
        when(productoRepository.findById(1L)).thenReturn(Optional.of(producto));

        Producto resultado = productoService.obtenerPorId(1L);

        assertNotNull(resultado);
        assertEquals("Tablet", resultado.getNombre());
    }

    @Test
    void testObtenerPorId_NoExiste() {
        when(productoRepository.findById(1L)).thenReturn(Optional.empty());

        Producto resultado = productoService.obtenerPorId(1L);

        assertNull(resultado);
    }
}

En el ejemplo anterior estamos probando:

11. Proyecto Spring. Cuestionarios.

Crearemos un proyecto de gestión de preguntas, empezando por el modelo de datos, los DTO, los repositorios y los servicios.

Debes asegurar de completar aquello que sea necesario, en especial los métodos toString, hashCode o equals, entre otros. Es sólo un esqueleto, casi funcional, pero incompleto.

Es una aplicación para la gestión de Cuestionarios.

Existen dos tipos de Preguntas:

Aplicación

La aplicación es una API REST que permite gestionar preguntas y cuestionarios. La aplicación está dividida en varias capas:

package com.example.apppreguntas;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.web.config.EnableSpringDataWebSupport;

import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO;

/**
 * Clase principal que inicia la aplicación Spring Boot para el sistema de gestión de preguntas.
 *
 * Esta clase configura los componentes esenciales de la aplicación y habilita soporte
 * para integración entre Spring Data y Spring Web.
 */
@SpringBootApplication // Anotación compuesta que incluye:
// - @Configuration: Marca la clase como fuente de definiciones de beans
// - @EnableAutoConfiguration: Habilita la configuración automática de Spring Boot
// - @ComponentScan: Habilita el escaneo de componentes en el paquete actual y subpaquetes
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) // Habilita soporte para integración entre Spring Data y Spring Web
// VIA_DTO indica que la paginación se serializará mediante DTOs
public class AppPreguntasApplication {

    /**
     * Método principal que inicia la aplicación Spring Boot.
     *
     * @param args Argumentos de línea de comandos (opcionales)
     */
    public static void main(String[] args) {
        SpringApplication.run(AppPreguntasApplication.class, args);
    }

}

Fichero de propiedades

Se debe trabajar con PostgreSQL, por lo que el fichero de propiedades es el siguiente (ojo, está en modo update, por lo que no es adecuado para producción):

spring.application.name=Cuestionarios
server.port=8080
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:postgresql://localhost:5432/Cuestionarios
spring.datasource.username=postgres
spring.datasource.password=
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.show-sql=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.globally_quoted_identifiers=true
  1. @SpringBootApplication:

    • Es una anotación compuesta que combina tres anotaciones fundamentales:
      • @Configuration: Identifica la clase como una fuente de definiciones de beans para el contexto de la aplicación.
      • @EnableAutoConfiguration: Habilita la configuración automática de Spring Boot, que configura automáticamente los beans que detecta en el classpath.
      • @ComponentScan: Habilita el escaneo de componentes en el paquete actual y sus subpaquetes, buscando clases anotadas con @Component, @Service, @Repository, etc.
  2. @EnableSpringDataWebSupport:

    • Habilita la integración entre Spring Data y Spring Web MVC.
    • Proporciona soporte para:
      • Conversión automática de parámetros de petición en objetos Pageable y Sort.
      • Soporte para la serialización de tipos de dominio como Point y Distance.
      • Registro de un LinkCollector para descubrir enlaces en las respuestas de Spring HATEOAS.
    • El parámetro pageSerializationMode = VIA_DTO:
      • Especifica cómo se deben serializar las páginas cuando se devuelven como parte de una respuesta REST.
      • VIA_DTO indica que las páginas deben serializarse mediante DTOs (Data Transfer Objects) en lugar de la implementación directa de Page.
      • Esto es más seguro y evita exponer detalles internos de implementación.
  3. SpringApplication.run():

    • Método estático que inicia la aplicación Spring Boot.
    • Crea el ApplicationContext adecuado (basado en el classpath).
    • Registra los beans definidos.
    • Inicia el servidor web embebido (si está en el classpath).

Así disponemos de:

  1. Configuración simplificada: @SpringBootApplication reduce la configuración manual necesaria.
  2. Integración web y datos: @EnableSpringDataWebSupport facilita el trabajo con paginación y ordenación en los controladores REST.
  3. Seguridad en serialización: El modo VIA_DTO protege contra la exposición accidental de información interna.
  4. Escaneo automático: Se detectan automáticamente todos los componentes, repositorios y controladores.

Esta configuración es ideal para aplicaciones REST que utilizan Spring Data JPA y necesitan soporte avanzado para paginación y ordenación en sus endpoints.

Subsecciones de 11. Proyecto Spring. Cuestionarios.

01. Modelo de datos

Modelo de datos

El modelo de datos debe estar almacenado en una base de datos relacional. En este caso, se ha optado por usar PostgreSQL como motor de base de datos, tal y como hemos hecho en los últimos ejercicios. La configuración de la base de datos se encuentra en el archivo application.properties del proyecto que ya hemos hecho otras veces.

Importante

Las clases indicadas están incompletas. En este ejercicio solo se muestran los atributos y las relaciones entre las entidades.

Modelo de datos Modelo de datos

Usuario

Debes añadir las relaciones y aquello que consideredes necesario.

package com.javhoz.ad.cuestionario.preguntas.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

public class Usuario {
    
    private Long idUsuario;

    private String login;
    private String password;
    private String email;
    private String nombreCompleto;

    public Usuario() {
    }

    public Usuario(Long idUsuario, String login, String password, String email, String nombreCompleto, List<Pregunta> preguntas) {
        this.idUsuario = idUsuario;
        this.login = login;
        this.password = password;
        this.email = email;
        this.nombreCompleto = nombreCompleto;
        this.preguntas = preguntas;
    }
    
    private List<Pregunta> preguntas = new ArrayList<>();

    // Getters y setters

    public Long getIdUsuario() { return idUsuario; }
    public void setIdUsuario(Long idUsuario) { this.idUsuario = idUsuario; }

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getNombreCompleto() { return nombreCompleto; }
    public void setNombreCompleto(String nombreCompleto) { this.nombreCompleto = nombreCompleto; }

    public List<Pregunta> getPreguntas() { return preguntas; }

    // Métodos de ayuda
    public void addPregunta(Pregunta p) {
        preguntas.add(p);
        p.setAutor(this);
    }

    public void removePregunta(Pregunta p) {
        preguntas.remove(p);
        p.setAutor(null);
    }

    @Override
    public String toString() {
        return "Usuario{" +
                "idUsuario=" + idUsuario +
                ", login='" + login + '\'' +
                ", password='[PROTEGIDO]'" +
                ", email='" + email + '\'' +
                ", nombreCompleto='" + nombreCompleto + '\'' +
                '}';
    }
}

Pregunta

Elige la estrategia de herencia que consideres más adecuada para el esquema anterior.

package com.ejemplo.app.model;

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;


public abstract class Pregunta {
    
    private Long idPregunta;
    private LocalDateTime fechaCreacion = LocalDateTime.now();

    private String enunciado;
    private Usuario autor;
    
    private Set<Etiqueta> etiquetas = new HashSet<>();

    // Getters y setters

    public Long getIdPregunta() { return idPregunta; }
    public void setIdPregunta(Long idPregunta) { this.idPregunta = idPregunta; }

    public LocalDateTime getFechaCreacion() { return fechaCreacion; }
    public void setFechaCreacion(LocalDateTime fechaCreacion) { this.fechaCreacion = fechaCreacion; }

    public String getEnunciado() { return enunciado; }
    public void setEnunciado(String enunciado) { this.enunciado = enunciado; }

    public Usuario getAutor() { return autor; }
    public void setAutor(Usuario autor) { this.autor = autor; }

    public Set<Etiqueta> getEtiquetas() { return etiquetas; }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
        Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
        if (thisEffectiveClass != oEffectiveClass) return false;
        Pregunta pregunta = (Pregunta) o;
        return getIdPregunta() != null && Objects.equals(getIdPregunta(), pregunta.getIdPregunta());
    }

    @Override
    public final int hashCode() {
        return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
    }
}

Cuestion

package com.javhoz.ad.cuestionario.preguntas.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;

import java.time.LocalDateTime;

public class Cuestion extends Pregunta {
    
    private String descripcion; // Debe contener un texto largo!!!!

    public Cuestion() {
    }

    public Cuestion(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, Usuario propietario, String descripcion) {
        super(idPregunta, fechaCreacion, enunciado, propietario);
        this.descripcion = descripcion;
    }

    public String getDescripcion() { return descripcion; }
    public void setDescripcion(String descripcion) { this.descripcion = descripcion; }

    @Override
    public String toString() {
        return "Cuestion{" +
                "idPregunta=" + getIdPregunta() +
                ", fechaCreacion=" + getFechaCreacion() +
                ", enunciado='" + getEnunciado() + '\'' +
                ", descripcion='" + descripcion + '\'' +
                '}';
    }
}

TipoTest

Las opciones deben borrarse en cascada, instanciación perezosa, y deben tener una relación con la pregunta.

package com.javhoz.ad.cuestionario.preguntas.model;

import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class TipoTest extends Pregunta {
    
    private List<Opcion> opciones = new ArrayList<>();

    public TipoTest() {
    }

    public TipoTest(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, Usuario propietario) {
        super(idPregunta, fechaCreacion, enunciado, propietario);
    }

    public List<Opcion> getOpciones() { return opciones; }

    public void addOpcion(Opcion opcion) {
        opciones.add(opcion);
        opcion.setPregunta(this);
    }

    public void removeOpcion(Opcion opcion) {
        opciones.remove(opcion);
    }

    public void removeOpcionById(Long id) {
        opciones.removeIf(o -> o.getIdOpcion().equals(id));
    }

    @Override
    public String toString() {
        return "TipoTest{" +
                "idPregunta=" + getIdPregunta() +
                ", fechaCreacion=" + getFechaCreacion() +
                ", enunciado='" + getEnunciado() + '\'' +
                ", opciones=" + opciones.size() +
                '}';
    }
}

Opcion

package com.javhoz.ad.cuestionario.preguntas.model;

import jakarta.persistence.*;

public class Opcion {
    private Long idOpcion;
    private String texto;
    private boolean correcta;
    private TipoTest pregunta;

    public Opcion() {
    }

    public Opcion(Long idOpcion, TipoTest pregunta, String texto, boolean correcta) {
        this.idOpcion = idOpcion;
        this.pregunta = pregunta;
        this.texto = texto;
        this.correcta = correcta;
    }

    // Getters y setters

    public Long getIdOpcion() {
        return idOpcion;
    }

    public void setIdOpcion(Long idOpcion) {
        this.idOpcion = idOpcion;
    }

    public TipoTest getPregunta() {
        return pregunta;
    }

    public void setPregunta(TipoTest pregunta) {
        this.pregunta = pregunta;
    }

    public String getTexto() {
        return texto;
    }

    public void setTexto(String texto) {
        this.texto = texto;
    }

    public boolean isCorrecta() {
        return correcta;
    }

    public void setCorrecta(boolean correcta) {
        this.correcta = correcta;
    }

    public boolean toggle() {
        return correcta = !correcta;
    }

    @Override
    public String toString() {
        return "Opcion{" +
                "idOpcion=" + idOpcion +
                ", texto='" + texto + '\'' +
                ", correcta=" + correcta +
                '}';
    }
}

Etiqueta

package com.javhoz.ad.cuestionario.preguntas.model;

import jakarta.persistence.*;
import org.hibernate.proxy.HibernateProxy;
import java.util.Objects;

public class Etiqueta {
    
    private Long idEtiqueta;
    private String nombre;
    public Etiqueta() {
    }

    public Etiqueta(Long idEtiqueta, String nombre) {
        this.idEtiqueta = idEtiqueta;
        this.nombre = nombre;
    }

    // Getters y setters

    public Long getIdEtiqueta() { return idEtiqueta; }
    public void setIdEtiqueta(Long idEtiqueta) { this.idEtiqueta = idEtiqueta; }

    public String getNombre() { return nombre; }
    public void setNombre(String nombre) { this.nombre = nombre; }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
        Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
        if (thisEffectiveClass != oEffectiveClass) return false;
        Etiqueta etiqueta = (Etiqueta) o;
        return getIdEtiqueta() != null && Objects.equals(getIdEtiqueta(), etiqueta.getIdEtiqueta());
    }

    @Override
    public final int hashCode() {
        return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
    }

    @Override
    public String toString() {
        return "Etiqueta{" +
                "idEtiqueta=" + idEtiqueta +
                ", nombre='" + nombre + '\'' +
                '}';
    }
}

Siguientes pasos

Con el modelo, seguiremos con la implementación de la lógica de acceso a datos. Para ello, crearemos los siguientes componentes:

  • Repositorios (interfaces con JpaRepository)
  • Servicios
  • Controladores REST
  • DTOs para separar las entidades del API
Última actualización: 23.09.2025

02. DTO

Los DTOs (Data Transfer Objects) son objetos que se utilizan para transferir datos entre diferentes capas de una aplicación, como la capa de presentación y la capa de negocio.

Los DTOs son útiles para encapsular datos y evitar la exposición directa de las entidades del modelo de dominio. Aquí los emplearemos para interactuar con la API REST.

Los DTOS (Data Transfer Objects) son objetos que se utilizan para transferir datos entre diferentes capas de una aplicación, como la capa de presentación y la capa de negocio:

  • Para recoger datos de la API REST.
  • Para reproducir el modelo para la respuesta
  • No es recomendable usar entidades para entrada y salida de datos, ya que pueden contener lógica de negocio y no son adecuadas para la serialización/deserialización.

Mapeo se emplea para Transformar un objeto de un tipo a otro.

  • De RequestDTO a Entity.
  • De Entity a ResponseDTO.

Para familiarizarte con el uso de DTOs, aquí tienes algunos ejemplos de DTOs que puedes usar en tu proyecto. Además, he empleado “record” que son data classes de Java que permiten crear objetos inmutables de forma sencilla con un constructor, getters y métodos equals y hashCode automáticamente.

OpcionResponse

package com.javhoz.ad.cuestionario.preguntas.dto;

import com.javhoz.ad.cuestionario.preguntas.model.Opcion;

/**
 * DTO para representar una opción de una pregunta de tipo test en las respuestas API
 * Contiene información básica de la opción: id, texto y si es correcta
 */
public record OpcionResponse(
        Long idOpcion,
        String texto,
        boolean correcta) {

    /**
     * Método factory para crear un OpcionResponse a partir de una entidad Opcion
     * @param opcion Entidad Opcion del modelo
     * @return DTO OpcionResponse
     */
    public static OpcionResponse of(Opcion opcion) {
        return new OpcionResponse(
                opcion.getIdOpcion(),
                opcion.getTexto(),
                opcion.isCorrecta());
    }
}

EditarCuestionRequest

package com.javhoz.ad.cuestionario.preguntas.dto;


/**
 * DTO para la edición de una pregunta básica (Cuestion)
 * Contiene los campos editables: enunciado y descripción
 * Se usa en las operaciones de actualización (PUT/PATCH)
 */
public record EditCuestionRequest(
        String enunciado,
        String descripcion) {
}

PreguntaRequest

package com.javhoz.ad.cuestionario.preguntas.dto;

import java.util.List;

/**
 * DTO para la creación de nuevas preguntas
 * Puede representar tanto preguntas básicas (Cuestion) como de tipo test (TipoTest)
 *
 * @param enunciado Texto principal de la pregunta
 * @param descripcion Descripción detallada (para Cuestion)
 * @param opciones Lista de opciones (para TipoTest)
 * @param username Nombre de usuario del creador
 */
public record PreguntaRequest(
        String enunciado,
        String descripcion,
        List<String> opciones,
        String username) {
}

PreguntaResponse

package com.javhoz.ad.cuestionario.preguntas.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.javhoz.ad.cuestionario.preguntas.model.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

/**
 * DTO principal para representar preguntas en las respuestas API
 * Usa JsonInclude para omitir campos nulos en la respuesta JSON
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public record PreguntaResponse(
        Long idPregunta,
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy HH:mm:ss")
        LocalDateTime fechaCreacion,
        String enunciado,
        String descripcion,
        List<OpcionResponse> opciones,
        String etiquetas) {

    // Constructor alternativo para preguntas básicas (Cuestion)
    public PreguntaResponse(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, String descripcion, String etiquetas) {
        this(idPregunta, fechaCreacion, enunciado, descripcion, null, etiquetas);
    }

    // Constructor alternativo para preguntas de tipo test (TipoTest)
    public PreguntaResponse(Long idPregunta, LocalDateTime fechaCreacion, String enunciado, List<OpcionResponse> opciones, String etiquetas) {
        this(idPregunta, fechaCreacion, enunciado, null, opciones, etiquetas);
    }

    /**
     * Factory method para crear respuesta a partir de una Cuestion
     * @param cuestion Pregunta básica
     * @return DTO PreguntaResponse
     */
    public static PreguntaResponse of(Cuestion cuestion) {
        return new PreguntaResponse(
                cuestion.getIdPregunta(),
                cuestion.getFechaCreacion(),
                cuestion.getEnunciado(),
                cuestion.getDescripcion(),
                cuestion.getEtiquetas().isEmpty() ? null :
                        cuestion.getEtiquetas()
                                .stream()
                                .map(Etiqueta::getNombre)
                                .collect(Collectors.joining(", ")));
    }

    /**
     * Factory method para crear respuesta a partir de un TipoTest
     * @param tipoTest Pregunta de tipo test
     * @return DTO PreguntaResponse
     */
    public static PreguntaResponse of(TipoTest tipoTest) {
        return new PreguntaResponse(
                tipoTest.getIdPregunta(),
                tipoTest.getFechaCreacion(),
                tipoTest.getEnunciado(),
                tipoTest.getOpciones()
                        .stream()
                        .map(OpcionResponse::of)
                        .toList(),
                tipoTest.getEtiquetas().isEmpty() ? null :
                        tipoTest.getEtiquetas()
                                .stream()
                                .map(Etiqueta::getNombre)
                                .collect(Collectors.joining(", ")));
    }

    /**
     * Factory method genérico que decide qué tipo de respuesta crear
     * según el tipo de pregunta recibida
     * @param pregunta Pregunta (puede ser Cuestion o TipoTest)
     * @return DTO PreguntaResponse adecuado
     */
    public static PreguntaResponse of(Pregunta pregunta) {
        if (pregunta instanceof Cuestion cuestion)
            return PreguntaResponse.of(cuestion);
        return PreguntaResponse.of((TipoTest) pregunta);
    }
}
Última actualización: 23.09.2025

03. Repositorios

Hemos trabajado bastante ya con repositorios, pero resulta importante ver cómo se pueden integrar con consultas personalizadas, con la paginación o parametrización de las mismas.

Tenemos tres repositorios, uno de ellos es polimórfico, Pregunta, y los otros dos son específicos para las entidades Etiqueta y Usuario.

  • Para petición POST de una nueva cuestión emplearemos PreguntaRequest, que es un DTO (Data Transfer Object) que contiene los datos necesarios para crear una nueva pregunta. Devolveremos 201 Created y la nueva pregunta en el cuerpo de la respuesta.
  • Para petición GET de una cuestión por ID emplearemos PreguntaResponse, que es un DTO que contiene los datos de la pregunta, incluyendo sus opciones y etiquetas.

Debes completar los métodos de los repositorios, en especial el de PreguntaRepository que es el más complejo.

PreguntaRepository

package com.javhoz.ad.cuestionario.preguntas.repositories;


import com.javhoz.ad.cuestionario.preguntas.model.Pregunta;
import jakarta.transaction.Transactional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface PreguntaRepository extends JpaRepository<Pregunta, Long> {

    /**
     * findAllWithOpcionesAndEtiquetas
     * Busca todas las preguntas con sus relaciones (opciones y etiquetas) cargadas
     * @param pageable Configuración de paginación
     * @return Página de preguntas con relaciones cargadas
     */


    /**
     * findByIdWithOpcionesAndEtiquetas
     * Busca una pregunta por ID con sus relaciones (opciones y etiquetas) cargadas
     * @param id ID de la pregunta
     * @return Optional con la pregunta si existe
     */


    /**
     * existsByIdAndPreguntaType
     * Verifica si existe una pregunta de un tipo específico por ID
     * @param preguntaType Tipo de pregunta (Cuestion.class o TipoTest.class)
     * @param id ID de la pregunta
     * @return true si existe, false en caso contrario
     */


    /**
     * toggleOpcionCorrecta
     * Emplea un query nativo para cambiar el estado de "correcta" de una opción (toggle)
     * Cambia el estado de "correcta" de una opción (toggle)
     * @param idPregunta ID de la pregunta de tipo test
     * @param idOpcion ID de la opción a modificar
     * @return Número de registros afectados
     */
    @Modifying
    @Transactional
    @Query(value = """
            Tu consulta SQL aquí
            """, nativeQuery = true)
    int toggleOpcionCorrecta(Long idPregunta, Long idOpcion);
}

EtiquetaRepository

package com.javhoz.ad.cuestionario.preguntas.repositories;

import com.javhoz.ad.cuestionario.preguntas.model.Etiqueta;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface EtiquetaRepository extends JpaRepository<Etiqueta, Long> {

    /**
     * Busca una etiqueta por su nombre
     * @param nombre Nombre de la etiqueta
     * @return Optional con la etiqueta si existe
     */
}

UsuarioRepository

package com.javhoz.ad.cuestionario.preguntas.repositories;

import com.javhoz.ad.cuestionario.preguntas.model.Usuario;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UsuarioRepository extends JpaRepository<Usuario, Long> {

    /**
     * Busca un usuario por su nombre de login
     * @param login Nombre de usuario
     * @return Optional con el usuario si existe
     */
}
Última actualización: 23.09.2025

04. Controller

El controller es la capa que se encarga de recibir las peticiones HTTP y devolver las respuestas correspondientes. En este caso, el controlador PreguntaController maneja las operaciones relacionadas con las preguntas, como crear, editar, eliminar y obtener preguntas.

Este controlador debe funcionar con el modelo implementado.

PreguntaController

package com.javhoz.ad.cuestionario.preguntas.controller;


import com.javhoz.ad.cuestionario.preguntas.dto.EditCuestionRequest;
import com.javhoz.ad.cuestionario.preguntas.dto.PreguntaRequest;
import com.javhoz.ad.cuestionario.preguntas.dto.PreguntaResponse;
import com.javhoz.ad.cuestionario.preguntas.model.*;
import com.javhoz.ad.cuestionario.preguntas.repositories.EtiquetaRepository;
import com.javhoz.ad.cuestionario.preguntas.repositories.PreguntaRepository;
import com.javhoz.ad.cuestionario.preguntas.repositories.UsuarioRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;
import java.util.Optional;

@RestController
@RequestMapping("/pregunta/")
public class PreguntaController {

    private final PreguntaRepository preguntaRepository;
    private final UsuarioRepository usuarioRepository;
    private final EtiquetaRepository etiquetaRepository;

    public PreguntaController(PreguntaRepository preguntaRepository,
                              UsuarioRepository usuarioRepository,
                              EtiquetaRepository etiquetaRepository) {
        this.preguntaRepository = preguntaRepository;
        this.usuarioRepository = usuarioRepository;
        this.etiquetaRepository = etiquetaRepository;
    }

    @PostMapping("/new/cuestion")
    public ResponseEntity<PreguntaResponse> newCuestion(@RequestBody PreguntaRequest preguntaRequest) {
        Optional<Usuario> propietario = usuarioRepository.findByLogin(preguntaRequest.username());

        Cuestion cuestion = new Cuestion();
        cuestion.setAutor(propietario.orElse(null));
        cuestion.setEnunciado(preguntaRequest.enunciado() != null ? preguntaRequest.enunciado() : "Sin enunciado");
        cuestion.setDescripcion(preguntaRequest.descripcion());

        cuestion = preguntaRepository.save(cuestion);

        URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/pregunta/{id}")
                .build(cuestion.getIdPregunta());

        return ResponseEntity.created(uri).body(PreguntaResponse.of(cuestion));
    }

    @PostMapping("/new/tipotest")
    public ResponseEntity<PreguntaResponse> newTipoTest(@RequestBody PreguntaRequest preguntaRequest) {
        Optional<Usuario> propietario = usuarioRepository.findByLogin(preguntaRequest.username());

        TipoTest tipoTest = new TipoTest();
        tipoTest.setAutor(propietario.orElse(null));
        tipoTest.setEnunciado(preguntaRequest.enunciado() != null ? preguntaRequest.enunciado() : "Sin enunciado");

        preguntaRequest.opciones().stream()
                .map(texto -> {
                    Opcion opcion = new Opcion();
                    opcion.setTexto(texto);
                    return opcion;
                })
                .forEach(tipoTest::addOpcion);

        tipoTest = preguntaRepository.save(tipoTest);

        URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/pregunta/{id}")
                .build(tipoTest.getIdPregunta());

        return ResponseEntity.created(uri).body(PreguntaResponse.of(tipoTest));
    }

    @GetMapping("/")
    public Page<PreguntaResponse> getAll(@PageableDefault(page=0, size=5, sort = "fechaCreacion") Pageable pageable) {
        Page<Pregunta> result = preguntaRepository.findAllWithOpcionesAndEtiquetas(pageable);

        if (result.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No se encontraron preguntas");

        return result.map(PreguntaResponse::of);
    }

    @GetMapping("/{id}")
    public PreguntaResponse getById(@PathVariable Long id) {
        return preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(PreguntaResponse::of)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND,
                        "Pregunta con ID %d no encontrada".formatted(id)));
    }

    @PutMapping("/cuestion/{id}")
    public ResponseEntity<PreguntaResponse> editCuestion(
            @RequestBody EditCuestionRequest editCuestionRequest,
            @PathVariable Long id) {

        if (!preguntaRepository.existsByIdAndPreguntaType(Cuestion.class, id)) {
            throw new ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Pregunta con ID %d no encontrada".formatted(id));
        }

        return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(Cuestion.class::cast)
                .map(cuestion -> {
                    cuestion.setEnunciado(editCuestionRequest.enunciado());
                    cuestion.setDescripcion(editCuestionRequest.descripcion());
                    return preguntaRepository.save(cuestion);
                })
                .map(PreguntaResponse::of));
    }

    @PutMapping("/tipotest/{id}/add/{opcion}")
    public ResponseEntity<PreguntaResponse> addOpcionToTipoTest(
            @PathVariable String opcion,
            @PathVariable Long id) {

        if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
            throw new ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Pregunta con ID %d no encontrada".formatted(id));
        }

        return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(TipoTest.class::cast)
                .map(tipoTest -> {
                    Opcion nuevaOpcion = new Opcion();
                    nuevaOpcion.setTexto(opcion);
                    tipoTest.addOpcion(nuevaOpcion);
                    return preguntaRepository.save(tipoTest);
                })
                .map(PreguntaResponse::of));
    }

    @DeleteMapping("/tipotest/{id}/del/{opcion_id}")
    public ResponseEntity<PreguntaResponse> deleteOpcionFromTipoTest(
            @PathVariable("opcion_id") Long opcionId,
            @PathVariable Long id) {

        if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
            throw new ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Pregunta con ID %d no encontrada".formatted(id));
        }

        return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(TipoTest.class::cast)
                .map(tipoTest -> {
                    tipoTest.removeOpcionById(opcionId);
                    return preguntaRepository.save(tipoTest);
                })
                .map(PreguntaResponse::of));
    }

    @PutMapping("/tipotest/{id}/toggle/{opcion_id}")
    public ResponseEntity<PreguntaResponse> toggleOpcionCorrecta(
            @PathVariable("opcion_id") Long opcionId,
            @PathVariable Long id) {

        if (!preguntaRepository.existsByIdAndPreguntaType(TipoTest.class, id)) {
            throw new ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Pregunta con ID %d no encontrada".formatted(id));
        }

        preguntaRepository.toggleOpcionCorrecta(id, opcionId);

        return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(PreguntaResponse::of));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> deletePregunta(@PathVariable Long id) {
        preguntaRepository.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    @PutMapping("/{id}/etiqueta/add/{etiqueta}")
    public ResponseEntity<PreguntaResponse> addEtiqueta(
            @PathVariable Long id,
            @PathVariable String etiqueta) {

        Etiqueta nuevaEtiqueta = etiquetaRepository.findByNombre(etiqueta)
                .orElseGet(() -> {
                    Etiqueta e = new Etiqueta();
                    e.setNombre(etiqueta);
                    return etiquetaRepository.save(e);
                });

        return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                .map(pregunta -> {
                    pregunta.getEtiquetas().add(nuevaEtiqueta);
                    return preguntaRepository.save(pregunta);
                })
                .map(PreguntaResponse::of));
    }

    @DeleteMapping("/{id}/etiqueta/del/{etiqueta}")
    public ResponseEntity<PreguntaResponse> deleteEtiqueta(
            @PathVariable Long id,
            @PathVariable String etiqueta) {

        Optional<Etiqueta> etiquetaExistente = etiquetaRepository.findByNombre(etiqueta);

        if (etiquetaExistente.isPresent()) {
            return ResponseEntity.of(preguntaRepository.findByIdWithOpcionesAndEtiquetas(id)
                    .map(pregunta -> {
                        pregunta.getEtiquetas().removeIf(e -> e.getNombre().equalsIgnoreCase(etiqueta));
                        return preguntaRepository.save(pregunta);
                    })
                    .map(PreguntaResponse::of));
        }

        return ResponseEntity.notFound().build();
    }
}
Última actualización: 23.09.2025

05. Pruebas y mejoras

Este proyecto proporciona un API REST completo para gestionar preguntas, opciones y etiquetas. A continuación, te mostraré cómo interactuar con los endpoints usando Postman:

https://www.postman.com/

Para ello, asegúrate de tener el servidor Spring Boot en funcionamiento. Puedes usar el comando mvn spring-boot:run para iniciar la aplicación.



Endpoints Disponibles

  • Preguntas Básicas (Cuestion)

    • POST /pregunta/new/cuestion: creación de una nueva pregunta básica
    • PUT /pregunta/cuestion/{id} → Actualizar una pregunta básica
  • Preguntas de Tipo Test (TipoTest)

    • POST /pregunta/new/tipotest: creación una pregunta de tipo test
    • PUT /pregunta/tipotest/{id}/add/{opcion}: añadir opción a una pregunta test
    • DELETE /pregunta/tipotest/{id}/del/{opcion_id}: eliminar una opción
    • PUT /pregunta/tipotest/{id}/toggle/{opcion_id}: marcar/desmarcar opción como correcta
  • Consultas

    • GET /pregunta/: listar todas las preguntas (paginadas)
    • GET /pregunta/{id}: obtener una pregunta por ID
  • Etiquetas

  • PUT /pregunta/{id}/etiqueta/add/{etiqueta}: añadir etiqueta a una pregunta

  • DELETE /pregunta/{id}/etiqueta/del/{etiqueta}: eliminar etiqueta

Postman

Para probar el API con Postman, sigue estos pasos:

1. Configuración Inicial

  • Descarga e instala Postman.
  • Importa la colección de endpoints (si está disponible).

2. Ejemplo: creación de una pregunta básica

🔹 Método: POST
🔹 URL: http://localhost:8080/pregunta/new/cuestion
🔹 Headers:

Content-Type: application/json

🔹 Body (JSON):

{
    "enunciado": "¿Qué es Spring Boot?",
    "descripcion": "Explica brevemente qué es Spring Boot.",
    "username": "javhoz"
}

Respuesta Esperada (201 Created):

{
    "idPregunta": 1,
    "fechaCreacion": "2025-05-20T10:00:00",
    "enunciado": "¿Qué es Spring Boot?",
    "descripcion": "Explica brevemente qué es Spring Boot."
}

En el proyecto debes incluir capturas de pantalla de las respuestas de Postman para cada uno de los endpoints.


Añadir Capa de Servicio

Actualmente, el controlador gestiona lógica de negocio, lo que no es ideal. Es preciso extraerla a una capa de servicio para mejorar la estructura.

Debes implantar el proyecto con una capa de servicio que gestione la lógica de negocio: PreguntaService.

  • Beneficios de emplear una capa de servicio:
    • Separación de responsabilidades: El controlador solo maneja HTTP.
    • Código más mantenible: La lógica de negocio está centralizada.
    • Más fácil de testear: Se pueden mockear servicios en pruebas unitarias.

Resumen

🔹 Usa Postman para probar el API fácilmente.
🔹 Refactoriza con una capa de servicio para mejorar la estructura del código.
🔹 Añade más funcionalidades:

  • Validaciones.
  • Manejo de errores.
  • Autenticación y autorización.
Última actualización: 23.09.2025

12. Proyecto Spring. Cuestionarios.

Práctica: API REST para Gestión de Preguntas y Cuestionarios

Objetivo

Se trata de desarrollar una API REST con Spring Boot que permita gestionar preguntas (básicas y tipo test) con opciones, etiquetas y usuarios, tal y como hemos visto en la práctica anterior. Además, se valorará la implementación opcional de una interfaz web con Thymeleaf para interactuar con el sistema.

A. Parte Obligatoria (API REST)

1. Modelo de Datos (40%)

El modelo de datos sigue la estructura de la práctica anterior, pero con algunas mejoras y adaptaciones para el uso de Spring Data JPA, ya que no se han implementado las relaciones, que deben estar correctamente definidas. La herencia debe estar perfectamente implementada, y las relaciones deben ser bidireccionales donde sea necesario.

2. Repositorios (20%)

Las consultas que se deben implantar están en el código proporcionado de la práctica.

3. DTOs y Controladores (20%)

Los DTOs y los controladores ya han sido definidos en la práctica anterior, pero deben adaptarse.

4. Capa de Servicio (20%)

Nota: La capa de servicio debe contener la lógica de negocio, separada de los controladores, y manejar las excepciones adecuadamente.

  • @Modifying es esencial para consultas de escritura en Spring Data JPA.

B. Parte Opcional (Interfaz Web con Thymeleaf) (Mejora de un 20%)

Rúbrica de Evaluación

Apartado Puntos Criterios
Modelo de datos 40% Correcta definición de entidades, relaciones y herencia.
Repositorios 20% Consultas optimizadas y uso de @Modifying para operaciones bulk.
DTOs y Controladores 20% JSON bien formateado y endpoints RESTful.
Capa de Servicio 20% Lógica separada correctamente y manejo de excepciones.
Interfaz Thymeleaf + 20% Funcionalidad completa (login, listado, formularios). Opcional.

Entrega



Ejemplos

1. Ejemplo de Petición (Postman)

Creaación de pregunta tipo test:

POST /pregunta/new/tipotest  
Body:  
{
  "enunciado": "Capitales de Europa",
  "opciones": ["Madrid", "Lisboa", "Berlín"],
  "username": "profesor1"
}

Solución Propuesta

  1. Repositorio:
public interface PreguntaRepository extends JpaRepository<Pregunta, Long> {
    @Query("SELECT p FROM Pregunta p LEFT JOIN FETCH p.opciones WHERE p.id = :id")
    Optional<Pregunta> findByIdWithOpciones(Long id);
}
  1. Servicio:
@Service
public class PreguntaService {
    public Pregunta crearTipoTest(PreguntaRequest request) {
        Usuario usuario = usuarioRepository.findByLogin(request.username())
            .orElseThrow(() -> new UsuarioNoEncontradoException(request.username()));
        // Lógica de creación...
    }
}
  1. Vista Thymeleaf (opcional):
<!-- listado.html -->
<div th:each="pregunta : ${preguntas}">
    <h3 th:text="${pregunta.enunciado}"></h3>
    <a th:href="@{/preguntas/{id}(id=${pregunta.id})}">Detalle</a>
</div>

2. @Modifying en Spring Data JPA

La anotación @Modifying se usa en Spring Data JPA para indicar que una consulta JPQL o SQL modificará datos (INSERT, UPDATE, DELETE). Sin ella, las consultas personalizadas con @Query son de solo lectura (read-only).

Uso:

Ejemplo:

import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface PreguntaRepository extends JpaRepository<Pregunta, Long> {
    
    @Modifying  // Indica que la consulta MODIFICA datos
    @Query("UPDATE Opcion o SET o.correcta = NOT o.correcta WHERE o.id = :opcionId")
    void toggleOpcionCorrecta(@Param("opcionId") Long opcionId);
}
  1. Requiere @Transactional (en el servicio o método).
  2. No retorna entidades, sino un void o el número de registros afectados (int).

3. Ejemplo Básico de Plantilla Thymeleaf

Para hacerlo necesitamos un controlador y una plantilla HTML que muestre datos dinámicos, itere sobre una lista y maneje formularios.

Thymeleaf

Thymeleaf es un motor de plantillas para Java que permite crear vistas dinámicas en aplicaciones web. Se integra fácilmente con Spring Boot y ofrece una sintaxis intuitiva para trabajar con datos del modelo.

Elementos clave de Thymeleaf:

Controlador

A continuación muestro un controlador de ejemplo que maneja una lista de preguntas y un formulario para añadir nuevas preguntas.

@Controller
public class PreguntaController {

    @GetMapping("/preguntas")
    public String listarPreguntas(Model model) {
        model.addAttribute("titulo", "Listado de Preguntas");
        model.addAttribute("preguntas", preguntaService.findAll());
        model.addAttribute("preguntaRequest", new PreguntaRequest());
        return "listado";  // Nombre de la plantilla Thymeleaf
    }
}

Plantilla Thymeleaf

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">  <!-- Namespace de Thymeleaf -->
<head>
    <meta charset="UTF-8">
    <title>Listado de Preguntas</title>
</head>
<body>
    <!-- 1. Mostrar datos dinámicos -->
    <h1 th:text="${titulo}">Título por defecto</h1>  <!-- th:text reemplaza el contenido -->

    <!-- 2. Iterar sobre una lista -->
    <ul th:each="pregunta : ${preguntas}">  <!-- th:each como un for-each -->
        <li>
            <span th:text="${pregunta.enunciado}">Enunciado</span>
            <a th:href="@{/preguntas/{id}(id=${pregunta.id})}">Ver detalle</a>  <!-- th:href para URLs dinámicas -->
        </li>
    </ul>

    <!-- 3. Formulario para añadir preguntas -->
    <form th:action="@{/preguntas/nueva}" method="post" th:object="${preguntaRequest}">
        <input type="text" th:field="*{enunciado}" placeholder="Enunciado">
        <button type="submit">Crear</button>
    </form>
</body>
</html>

Elementos de la plantilla:

Atributo Función
th:text Muestra texto dinámico (ej: "${variable}").
th:each Itera sobre colecciones (como v-for en Vue o *ngFor en Angular).
th:href Genera URLs dinámicas (ej: @{/ruta/{id}(id=${variable})}).
th:action Define la acción del formulario (enlace al endpoint del controlador).
th:object Vincula el formulario a un objeto DTO (th:field mapea los campos).

Ejemplo con autenticación

<!-- navbar.html -->
<div th:if="${#authentication.isAuthenticated()}">  <!-- th:if para condicionales -->
    <span th:text="${#authentication.name}">Usuario</span>
    <a th:href="@{/logout}">Cerrar sesión</a>
</div>

Integración en el proyecto

Este sencillo ejemplo muestra cómo integrar los conceptos anteriores en un proyecto Spring Boot con Thymeleaf y Spring Security en una aplicación de gestión de preguntas.

Para ello se precisa un Controller, una configuración de seguridad y una estructura de plantillas.

  1. Controlador:
@Controller
public class PreguntaController {
    
    @GetMapping("/preguntas") // Ruta para listar preguntas
    public String listarPreguntas(Model model) {
        model.addAttribute("titulo", "Listado de Preguntas"); // Este atributo se usará en la vista (template)
        model.addAttribute("preguntas", preguntaService.findAll()); // Añade la lista de preguntas al modelo
        return "listado";  // Renderiza listado.html
    }
}
  1. Spring Security:
  1. Carpeta de plantillas:

UD 5. Bases de datos no SQL. MongoDB

UD 4. Bases de datos no SQL. MongoDB

1. ¿Qué son las bases de datos NoSQL?

Durante un cuarto de siglo, las bases de datos relacionales (RDBMS) han sido el modelo dominante para la gestión de bases de datos. Pero, hoy en día, las bases de datos no relacionales, “cloud” o “NoSQL” están ganando terreno como modelo alternativo para la gestión de bases de datos.

En el mundo de la tecnología de bases de datos, existen dos tipos principales de bases de datos:

La diferencia radica en cómo están construidas, el tipo de información que almacenan y cómo la almacenan.

Las bases de datos relacionales están estructuradas, como las guías telefónicas que almacenan números de teléfono y direcciones. Las bases de datos no relacionales no almacenan datos de forma tabular y están orientadas los documentos, grafos, diccionarios… También suelen estar distribuidas y proporcionan esquemas flexibles y escalables con grandes cantidades de datos y cargas de usuarios.

Subsecciones de UD 5. Bases de datos no SQL. MongoDB

01. Bases de datos NoSQL.

1. Introducción

Las bases de datos desempeñan un papel importante en la informática, ya que son el mecanismo central para almacenar, organizar, gestionar y recuperar grandes cantidades de datos.

En cuanto a los sistemas de gestión de bases de datos, existen dos tipos básicos:

BD SQL vs NOSQL BD SQL vs NOSQL

Si los requisitos de datos no están claros desde el principio o si estamos tratando con cantidades masivas de datos no estructurados, quizás no podamos permitirnos diseñar una base de datos relacional con un esquema claramente definido. En este caso, utilizar bases de datos no relacionales nos ofrece una mayor flexibilidad.

Podemos ver las bases de datos no relacionales como un conjunto de directorios que contienen ficheros, que almacenan información relacionada de todo tipo. Por ejemplo, si un blog usase una base de datos NoSQL, se podría almacenar un fichero con la información de cada post: fotos, texto, métricas, enlaces, etc.

El hecho de intentar almacenar, procesar y analizar datos no estructurados dio lugar al desarrollo de herramientas alternativas a SQL. Estas herramientas se conocen como NoSQL (Not only SQL).

2. Bases de Datos SQL

Las bases de datos SQL (Structured Query Language) proporcionan precisión de la información al mantener una fuerte consistencia de datos y admitir transacciones complejas. Son adecuadas para operaciones que requieren datos organizados y un alto nivel de integridad de datos debido a su enfoque unificado, que les permite trabajar con varios frameworks y plataformas:

BD SQL BD SQL

Además, mantienen el modelo relacional, que divide la información en tablas con filas y columnas. Este tipo de base de datos crea un esquema predeterminado que explica la estructura de los datos y las relaciones.

Las bases de datos SQL consultan, alteran y gestionan datos utilizando el lenguaje SQL:

Son algunas de las bases de datos SQL más populares.

3. Características de las Bases de Datos SQL

El esquema es la característica inicial de una base de datos SQL.

Un esquema describe la estructura de la base de datos, incluyendo tablas, columnas, tipos de datos y relaciones entre tablas. Esta característica clave asegura la consistencia de los datos y permite búsquedas e indexación rápidas.

Las bases de datos SQL se benefician de la amplia implantación y soporte del lenguaje SQL. SQL permite a los desarrolladores utilizar una única sintaxis para construir consultas, realizar uniones complejas y modificar datos, junto con una gran cantidad de herramientas, bibliotecas y marcos que facilitan la gestión, informes y análisis de datos.

Las bases de datos SQL proporcionan las propiedades ACID (Atomicidad, Consistencia, Aislamiento y Durabilidad), que aseguran la integridad de los datos y la confiabilidad transaccional.

4. Bases de Datos NoSQL

Las bases de datos NoSQL significan “no solo SQL”, indicando que estas bases de datos no se limitan al modelo relacional tradicional.

Estas bases de datos sobresalen en situaciones donde las estructuras de datos pueden ser variadas, y la capacidad para manejar datos dinámicos, no estructurados o semiestructurados es fundamental:

BD NoSQL BD NoSQL

Una ventaja principal de las bases de datos NoSQL es su capacidad para crecer horizontalmente, lo que les permite manejar cantidades enormes de datos y cargas de tráfico pesadas de manera efectiva.

Logran esta escalabilidad distribuyendo datos en numerosos nodos de clúster, permitiendo el procesamiento simultáneo.

Las bases de datos NoSQL admiten la disponibilidad sobre la consistencia robusta, proporcionando lo que se podría llamar “consistencia eventual”. Esto implica que las modificaciones de la base de datos pueden tardar tiempo en propagarse a través de todos los nodos, aumentando la disponibilidad, confiabilidad y tolerancia a fallos.

Exiten muchos frameworks o utilidades para gestionar y analizar fácilmente diversos tipos de datos y expandir las aplicaciones para abordar cantidades crecientes de datos eligiendo un tipo de base de datos NoSQL adecuado.

5. Tipos de Bases de Datos NoSQL

Existen cuatro tipos principales de bases de datos NoSQL:

5.1. Bases de Datos Orientadas a Documentos

Las bases de datos orientadas a documentos almacenan datos en documentos flexibles y autoencriptados (como JSON o XML) que pueden modificarse rápidamente sin afectar toda la base de datos.

Son apropiadas para manejo de estructuras de datos jerárquicas.

Están diseñadas para almacenar datos en documentos que pueden tener diferentes formas en función de la información a almacenar.

Representan las relaciones utilizando subdocumentos y/o arrays incrustados en un único documento.

El documento es análogo al objeto en la programación orientada a objetos y proporciona una representación clara y natural de una entidad del mundo real y sus datos. Esta clara representación provoca que no sea necesario realizar un mapeo entre la base de datos y la aplicación.

El documento permite, a menudo, representar de forma exacta el objeto que el programador desea utilizar. La flexibilidad del documento a la hora de almacenar información en múltiples formatos al mismo tiempo proporciona también una gran flexibilidad a la hora de realizar el modelado.

Ejemplo de modelo de documento Ejemplo de modelo de documento{width=“5.9in” height=“3.0833333333333335in”}

El aspecto clave a comprender en este tipo de bases de datos es que los datos relacionados se almacenan de forma conjunta, pero no tienen por qué tener el mismo formato. Esto es, un documento puede estar relacionado con otro y estar almacenados de forma conjunta, pero no tienen por qué contener los mismos campos de datos.

5.2. Bases de Datos Clave-Valor

Los gestores de datos clave-valor almacenan datos como pares simple clave-valor, ofreciendo un rendimiento respetable para aplicaciones de alto rendimiento y se utilizan principalmente para aplicaciones web de alto tráfico.

El esquema es muy simple: una clave única está relacionada con una serie de valores, que pueden ser desde un string hasta un objeto binario.

La ventaja de este tipo de bases de datos es que las consultas son muy simples. El sistema sabe en qué servidor se localiza la información y envía la petición a ese servidor.

No es recomendable cuando nuestro esquema tiene relaciones complejas.

Base de datos clave-valor - Wikipedia, la enciclopedia\nlibre Base de datos clave-valor - Wikipedia, la enciclopedia\nlibre

5.3. Bases de Datos Columnares

Otra base de datos NoSQL bastante popular es la base de datos columnar, que almacena datos en columnas en lugar de filas, lo que las hace adecuadas para cargas de trabajo analíticas que implican la búsqueda de atributos específicos en conjuntos de datos grandes.

Están diseñadas principalmente para análisis de datos.

La ventaja es que devuelven los datos en columnas, haciendo que las consultas sean mucho más eficientes, de forma que no devuelvan datos inútiles.

La clave primaria en estas bases de datos es el dato/valor que después es mapeado a las claves de las filas. Esto es opuesto a una clave primaria en una base de datos relacional.

La estructura de los datos que están en las columnas es flexible y puede variar de fila a fila.

5.4. Bases de Datos Orientadas a Grafos

Las bases de datos de grafos se centran en el almacenamiento y la búsqueda de asociaciones entre entidades, lo que las hace adecuadas para redes complejas o redes sociales, motores de recomendación o detección de fraudes.

Están diseñadas para tratar con problemas de relaciones y se centran en la conexión de los datos. Tiene la ventaja de que permite representar relaciones complejas. Sin embargo, muchos problemas no se modelan así de forma natural.

La información se almacena como una colección de nodos y aristas, donde las aristas representan las relaciones entre los nodos. El hecho de almacenar las relaciones entre los datos permite que los datos relacionados se puedan recuperar en una sola operación.

6. Ejemplos de Bases de Datos NoSQL

Ejemplos típicos de bases de datos NoSQL incluyen:

7. Características de las Bases de Datos NoSQL

Esquema flexible, escalabilidad, consistencia eventual y casos de uso específicos son las características clave de las bases de datos NoSQL.

La característica diferencial de las bases de datos NoSQL es el esquema flexible, que permite la estructuración de datos flexible y dinámica.

Las bases de datos NoSQL ofrecen una variedad de estructuras, haciéndolas adecuadas cuando los modelos de datos están evolucionando o al tratar con datos no estructurados o semiestructurados. Esta flexibilidad elimina las costosas transiciones de esquema y permite la prototipación rápida y la reutilización.

La escalabilidad es otra característica crucial de las bases de datos NoSQL. Estas bases de datos están diseñadas para manejar grandes volúmenes de datos y cargas de tráfico elevadas, lo que las hace altamente escalables horizontalmente. Las bases de datos NoSQL distribuyen datos en múltiples nodos en un clúster y, como resultado, pueden acomodar mayores demandas de datos y usuarios, así como procesamiento paralelo.

La “consistencia eventual”, que significa preferir la disponibilidad y la tolerancia a particiones sobre una estricta consistencia de datos. Este compromiso permite una alta disponibilidad y tolerancia a fallos, ya que el sistema puede seguir operando incluso en presencia de particiones de red o fallos de nodos.

Las bases de datos NoSQL suelen estar diseñadas con casos de uso específicos en mente.

8. Bases de Datos SQL vs. NoSQL

Comprender las ventajas y desventajas de las bases de datos SQL y NoSQL es crucial, ya que satisfacen ciertas necesidades de aplicación.

8.1. ACID vs. BASE

Las bases de datos SQL o relacionales están focalizadas hacia la fiabilidad de las transacciones, modelo ACID (Atomicity, Consistency, Isolation, Durability).

Las bases de datos NoSQL es más relevante cuando la magnitud y dinamismo de los datos cobran importancia y el modelo ACID de los modelos relacionales queda en segundo plano frente al rendimiento, disponibilidad y escalabilidad, que son características más propias de las bases de datos NoSQL o no relacionales.

Hoy en día, los sistemas de almacenamiento de datos en Internet se ajustan más al conocido como modelo BASE (Basic Availability, Soft state, Eventually consistency), aunque también hay bases de datos NoSQL compatibles con ACID. El modelo BASE permite obtener una flexibilidad máxima.

Sus principales diferencias se destacan en la siguiente tabla:

Característica Bases de Datos SQL Bases de Datos NoSQL
Modelo de Datos Relacional (Tablas) Variable (Documentos, Clave-Valor, Columnar, Grafos, etc.)
Esquema Fijo (Requiere esquema definido) Flexible (Permite esquema dinámico)
Escalabilidad Vertical (Aumento de capacidad en el servidor) Horizontal (Aumento de capacidad mediante la adición de nodos)
Consistencia ACID (Consistencia estricta) Eventual (Consistencia eventual)

9. ¿Cuándo usar bases de datos NoSQL?

9.1. Aplicaciones de Bases de Datos SQL

Las aplicaciones que requieren una sólida integridad de datos, modelos de datos organizados y transacciones intrincadas son más adecuadas para las bases de datos SQL. Rendimiento excepcional en situaciones que involucran sistemas financieros, plataformas de comercio electrónico y aplicaciones corporativas convencionales donde la integridad y confiabilidad de los datos son cruciales.

9.2. Aplicaciones de Bases de Datos NoSQL

Por otro lado, las bases de datos NoSQL son una mejor opción para programas que necesitan alta escalabilidad y modelos de datos adaptables. Sobresalen en aplicaciones como:

En las cuales las estructuras de datos dinámicas incluyen grandes volúmenes de datos no estructurados o semiestructurados.

Las bases de datos NoSQL (sobre todo las documentales) son apropiadas para almacenar datos polimórficos que pueden cambiar frecuentemente. Por ejemplo, el modelo documental permite almacenar en la misma colección datos con diferentes formas, lo que significa que los documentos pueden tener campos diferentes.

Se pueden modificar los campos de un documento sin preocuparnos por el impacto o “efectos secundarios” que esto pueda tener sobre la base de datos. Por ejemplo, la base de datos no necesita ser actualizada cuando necesitamos añadir un nuevo campo, algo que sí pasa con las bases de datos relacionales.

Otro aspecto a destacar de las bases de datos NoSQL es que nos permiten representar los objetos de una forma muy fiel y directa. Esto significa que podemos intercambiar datos entre la aplicación y la base de datos sin necesidad de mapearlos. Esto también mejora la productividad, ya que no necesitamos código para realizar traducciones entre la aplicación y la base de datos.

Los sistemas NoSQL típicamente son “cloud-native” y están diseñados de forma distribuida, lo que resulta interesante si necesitamos tener una base de datos fácilmente escalable.

La decisión entre bases de datos SQL y NoSQL, o un enfoque híbrido que combina las fortalezas de ambas, depende en última instancia de los requisitos específicos de la aplicación y los datos. La consistencia de datos, las necesidades de escalabilidad, la velocidad de desarrollo, la flexibilidad y el soporte del ecosistema deben ser considerados.

03. Conexión a MongoDB con String Data MongoDB (práctica).

1. Accediendo a Datos con MongoDB

En esta práctica trabajaremos con un proyecto Spring Data MongoDB para construir una aplicación que almacena datos en MongoDB.

Almacenaremos objetos Usuario POJO (Plain Old Java Objects) en una base de datos MongoDB utilizando Spring Data MongoDB.

Requisitos:

2. Spring Initializr

Para crear el proyecto:

  1. Ve a Spring Initializr. Este servicio incorpora todas las dependencias que necesitas para una aplicación y realiza la mayor parte de la configuración.
  2. Elige Maven y el lenguaje Java.
  3. Haz clic en Dependencies y selecciona Spring Data MongoDB.
  4. Haz clic en Generate.
  5. Descarga el archivo ZIP resultante, que es un archivo comprimido de una aplicación web.

Como hemos visto, IntellJ Ultimate, Eclipse o Visual Studio Code, disponen de un Spring Initializr que permite crearlo sin acceder a la página Web de Spring Boot.

3. Instalación e inicio de MongoDB

Con tu proyecto configurado, puedes instalar y lanzar la base de datos MongoDB.

Instalación de MongoDB en Mac

Si usas una Mac con Homebrew, puedes ejecutar el siguiente comando:

$ brew install mongodb

Con MacPorts, puedes ejecutar el siguiente comando:

$ port install mongodb

Instalación de MongoDB en Windows y otras plataformas

Para otros sistemas con gestores de paquetes, como Redhat, Ubuntu, Debian, CentOS y Windows, consulta las instrucciones en https://docs.mongodb.org/manual/installation/.

Iniciar MongoDB

Después de instalar MongoDB, puedes iniciarlo en una ventana de consola ejecutando el siguiente comando (que también inicia un proceso de servidor):

$ mongod

Deberías ver una salida similar a la siguiente:

all output going to: /usr/local/var/log/mongodb/mongo.log

4. Creación de una “Endidad” Usuario

MongoDB es una base de datos documental NoSQL. En esta práctica, almecenaremos objetos Usuario. El siguiente código muestra la clase Usuario (en src/main/java/com/javhoz/ad/mongodb/Usuario.java):

package com.javhoz.ad.mongodb;

import org.springframework.data.annotation.Id;

public class Usuario {

  @Id
  public String idUsuario;

  public String nome;
  public String apelidos;

  public Usuario() {}

  public Usuario(String nome, String apelidos) {
    this.nome = nome;
    this.apelidos = apelidos;
  }

  @Override
  public String toString() {
    return String.format(
        "Usuario[idUsuario=%s, nome='%s', apelidos='%s']",
        idUsuario, nome, apelidos);
  }
  
  // getters y setters
    

}

La clase Usuario tiene tres atributos: idUsuario, nome y apelidos. El idUsuario es principalmente para uso interno de MongoDB. También dispone un constructor único para crear una nueva instancia y el constructor por defecto.

Se omiten los típicos getters y setters para simplificar el código. El idUsuario no se ajusta al nombre estándar para un ID de MongoDB, pero podría emplearse una anotación para etiquetarlo para Spring Data MongoDB.

Para ello, añade la anotación @Id a la propiedad idUsuario. También podrías usar la anotación @Field para especificar el nombre del campo en la base de datos.

Las otras dos propiedades, nome y apelidos, se dejan sin anotación. Se asume que se asignan a campos que comparten el mismo nombre que las propiedades mismas.

El conveniente sobrescribir el método toString() imprime los detalles sobre un cliente.

MongoDB almacena datos en colecciones (equivalente a una table en un SGBDR). Spring Data MongoDB mapea la clase Usuario a una colección llamada usuario. Si deseas cambiar el nombre de la colección, puedes usar la anotación @Document de Spring Data MongoDB en la clase.

5. Creación de consultas con Spring Data MongoDB

Spring Data MongoDB se centra en almacenar datos en MongoDB. Como hereda funcionalidades del proyecto Spring Data Commons, tiene la capacidad de derivar consultas. Esencialmente, no se necesita aprender el lenguaje de consulta de MongoDB. Puedes emplear muchos métodos y las consultas se escriben de manera automática.

Creación de un Repositorio

Para ver cómo funciona esto, crea una interfaz de repositorio que consulte documentos de Usuario, como se muestra en el siguiente código (en src/main/java/com/javhoz/ad/mongodb/RepositorioUsuario.java):

package com.javhoz.ad.mongodb;

import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface RepositorioUsuario extends MongoRepository<Usuario, String> {

  public Usuario findByNome(String nome);
  public List<Usuario> findByApelidos(String apelidos);

}

RepositorioUsuario extiende la interfaz MongoRepository e incorpora el tipo de valores e ID con los que trabaja: Usuario y String, respectivamente. Esta interfaz viene con muchas operaciones, incluidas las operaciones CRUD estándar (crear, leer, actualizar y eliminar).

Puedes definir otras consultas declarando sus firmas de método. En este caso, agrega findByNome, que esencialmente busca documentos de tipo Usuario y encuentra los documentos que coinciden con nome.

Las lista completa de palabras clave que puedes usar en MongoRepository se encuentra en la documentación de Spring Data MongoDB.

También he añadido findByApelidos, que encuentra una lista de personas por apellido.

En una aplicación Java típica, se implementaría una clase que implemente RepositorioUsuario y se crearía la consulta personalizada. Spring Data MongoDB no necesitas crear esta implementación, lo que lo hace muy útil. Spring Data MongoDB la crea dinámicamente cuando ejecutas la aplicación.

6. Crear una Clase de Aplicación

Spring Initializr crea una clase sencilla para la aplicación. El siguiente código muestra la clase que Initializr creó para este ejemplo (en src/main/java/com/javhoz/ad/mongodb/AccesoDatosMongodbApplication.java, pero depende del nombre que le hayas dado al proyecto en el inicializador de Spring):

package com.javhoz.ad.mongodb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccesoDatosMongodbApplication {

  public static void main(String[] args) {
    SpringApplication.run(AccesoDatosMongodbApplication.class, args);
  }

}

@SpringBootApplication es una anotación útil que añade todo lo siguiente:

El método main() utiliza el método SpringApplication.run() de Spring Boot para lanzar una aplicación. Esta aplicación web es 100% Java puro y no hay que “pelearse” con la configuración de la estructura (web.xml, etc.).

Gestión de repositorios

Spring Boot gestiona automáticamente esos repositorios siempre que estén incluidos en el mismo paquete (o un subpaquete) que tu clase @SpringBootApplication. Para tener un mayor control sobre el proceso de registro, puedes usar la anotación @EnableMongoRepositories.

Por defecto, @EnableMongoRepositories escanea el paquete actual en busca de interfaces que extiendan una de las interfaces de repositorio de Spring Data.

Se puede usar basePackageClasses=MiRepository.class para decirle de manera segura a Spring Data MongoDB que escanee un paquete raíz diferente por tipo si la disposición de tu proyecto tiene múltiples proyectos y no encuentra tus repositorios.

Spring Data MongoDB utiliza MongoTemplate para ejecutar las consultas detrás de tus métodos find*. Puedes usar la plantilla tú mismo para consultas más complejas, de momento lo dejamos así (podéis consultar la Guía de Referencia de Spring Data MongoDB para más detalles):

https://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb/core/MongoTemplate.html

Modificaremos la clase de la aplicación que creó Initializr para ti que cree documentos, para configurar algunos datos y usarlos para generar salida.

El siguiente código muestra la clase AccesoDatosMongodbApplication completa (en src/main/java/com/javhoz/ad/mongodb/AccesoDatosMongodbApplication.java):

package com.javhoz.ad.mongodb;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AccesoDatosMongodbApplication implements CommandLineRunner {

  @Autowired
  private RepositorioUsuario repository;

  public static void main(String[] args) {
    SpringApplication.run(AccesoDatosMongodbApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {

    repository.deleteAll();

    // guarda varios usuarios, con nombres y apellidos de mujeres científicas
    repository.save(new Usuario("Ada", "Lovelace")); // Ada Lovelace fue una matemática y escritora británica, conocida por su trabajo sobre la máquina analítica de Charles Babbage
    repository.save(new Usuario("Marie", "Curie")); // Marie Curie fue una científica polaca, nacionalizada francesa, pionera en el campo de la radiactividad
    repository.save(new Usuario("Grace", "Hopper")); // Grace Hopper fue una científica de la computación y almirante de la Marina de los Estados Unidos. Fue una de las primeras programadoras de la historia.
    repository.save(new Usuario("Barbara", "Liskov")); // Barbara Liskov es una científica de la computación que desarrolló el lenguaje de programación CLU y fue la primera mujer en recibir un doctorado en informática en los Estados Unidos

    // Muestra todos los usuarios
    System.out.println("Usuarias encontradas con findAll():");
    System.out.println("-------------------------------");
    for (Usuario usuario : repository.findAll()) {
      System.out.println(usuario);
    }
    System.out.println();

    // Busca un usuario por su nombre
    System.out.println("Usuario encontrado con findByNome('Marie'):");
    System.out.println("--------------------------------");
    System.out.println(repository.findByNome("Marie"));

    System.out.println("Usuarios encontrados con findByApelidos('Liskov'):");
    System.out.println("--------------------------------");
    for (Usuario usuario : repository.findByApelidos("Liskov")) {
      System.out.println(usuario);
    }

  }

}

AccesoDatosMongodbApplication incluye un método main() que realiza la inyección de dependencias de una instancia de RepositorioUsuario.

Spring Data MongoDB crea dinámicamente un proxy y lo inyecta ahí.

Utilizamos RepositorioUsuario a través de algunas pruebas: a) Elimina todas las entidades con deleteAll(), demostrando el método deleteAll() y configurando algunos datos para usar. b) Llama a findAll() para obtener todos los objetos Usuario de la base de datos. c) Invoca a findByNome() para obtener un único Usuario por su nombre. d) findByApelidos() para encontrar todos los clientes cuyo apellido es Liskov.

Por defecto, Spring Boot intenta conectarse a una instancia de MongoDB alojada localmente. Lee la documentación de referencia para obtener detalles sobre cómo apuntar tu aplicación a una instancia de MongoDB alojada en otro lugar. Para hacerlo, puedes configurar una conexión a MongoDB en el archivo application.properties:

spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=mydatabase

7. Construir un JAR Ejecutable

Puedes ejecutar la aplicación desde la línea de comandos con Gradle o Maven. También puedes construir un solo archivo JAR ejecutable que contenga todas las dependencias, clases y recursos necesarios y ejecutarlo. Construir un JAR ejecutable facilita el envío, versionado e implementación del servicio como una aplicación a lo largo del ciclo de desarrollo, en diferentes entornos, y así sucesivamente.

Si usas Gradle, puedes ejecutar la aplicación con ./gradlew bootRun. Alternativamente, puedes construir el archivo JAR con ./gradlew build y luego ejecutar el archivo JAR, así:

java -jar build/libs/gs-acceso-datos-mongodb-0.1.0.jar

Si usas Maven, puedes ejecutar la aplicación con ./mvnw spring-boot:run. Alternativamente, puedes construir el archivo JAR con ./mvnw clean package y luego ejecutar el archivo JAR, así:

java -jar target/gs-acceso-datos-mongodb-0.1.0.jar

Los pasos descritos aquí crean un JAR ejecutable. También puedes construir un archivo WAR clásico.

Dado que AccesoDatosMongodbApplication implementa CommandLineRunner, el método run se invoca automáticamente cuando Spring Boot se inicia. Deberías ver algo como lo siguiente (con otra salida, como consultas, también):

== Usuarias encontradas con findAll():
-------------------------------
Usuario[idUsuario=51df1b0a3004cb49c50210f8, nome='Ada', apelidos='Lovelace']
Usuario[idUsuario=51df1b0a3004cb49c50210f9, nome='Marie', apelidos='Curie']
Usuario[idUsuario=51df1b0a3004cb49c50210fa, nome='Grace', apelidos='Hopper']
Usuario[idUsuario=51df1b0a3004cb49c50210fb, nome='Barbara', apelidos='Liskov']

== Usuario encontrado con findByNome('Marie'):
--------------------------------
Usuario[idUsuario=51df1b0a3004cb49c50210f9, nome='Marie', apelidos='Curie']

== Usuarios encontrados con findByApelidos('Liskov'):
--------------------------------
Usuario[idUsuario=51df1b0a3004cb49c50210fb, nome='Barbara', apelidos='Liskov']

Git

Lista de vídeos

Enlace a los vídeos sobre GIT

Nota importante

Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2

Subsecciones de Git

Introducción git

Nota importante

Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2

Subsecciones de Introducción git

Introducción a git

Control de Versiones

Un control de versiones es un sistema que registra los cambios realizados en un archivo o conjunto de archivos a lo largo del tiempo, de modo que puedas recuperar versiones específicas más adelante.

Usar un VCS (Sistema de Control de Versiones) también significa generalmente que si arruinas o pierdes archivos, será posible recuperarlos fácilmente. Adicionalmente, obtendrás todos estos beneficios a un costo muy bajo.

Sistemas de control de versiones locales

Un método de control de versiones, usado por muchas personas, es copiar los archivos a otro directorio. Este método es muy común porque es muy sencillo, pero también es tremendamente propenso a errores. Es fácil olvidar en qué directorio te encuentras y guardar accidentalmente en el archivo equivocado o sobrescribir archivos que no querías. Para afrontar este problema los programadores desarrollaron hace tiempo VCS locales que contenían una simple base de datos, en la que se llevaba el registro de todos los cambios realizados a los archivos.

Sistemas de control de versiones centralizados

El siguiente gran problema con el que se encuentran las personas es que necesitan colaborar con desarrolladores en otros sistemas. Los sistemas de Control de Versiones Centralizados (CVCS por sus siglas en inglés) fueron desarrollados para solucionar este problema. Esta configuración ofrece muchas ventajas, especialmente frente a VCS locales. Por ejemplo, todas las personas saben hasta cierto punto en qué están trabajando los otros colaboradores del proyecto. Los administradores tienen control detallado sobre qué puede hacer cada usuario, y es mucho más fácil administrar un CVCS que tener que lidiar con bases de datos locales en cada cliente.

Sin embargo, esta configuración también tiene serias desventajas. La más obvia es el punto único de fallo que representa el servidor centralizado. Si ese servidor se cae durante una hora, entonces durante esa hora nadie podrá colaborar o guardar cambios en archivos en los que hayan estado trabajando. Si el disco duro en el que se encuentra la base de datos central se corrompe, y no se han realizado copias de seguridad adecuadamente, se perderá toda la información del proyecto, con excepción de las copias instantáneas que las personas tengan en sus máquinas locales.

Sistemas de control de versiones Distribuidos

Los sistemas de Control de Versiones Distribuidos (DVCS por sus siglas en inglés) ofrecen soluciones para los problemas que han sido mencionados. En un DVCS (como Git, Mercurial, Bazaar o Darcs), los clientes no solo descargan la última copia instantánea de los archivos, sino que se replica completamente el repositorio. De esta manera, si un servidor deja de funcionar y estos sistemas estaban colaborando a través de él, cualquiera de los repositorios disponibles en los clientes puede ser copiado al servidor con el fin de restaurarlo. Cada clon es realmente una copia completa de todos los datos.

Además, muchos de estos sistemas se encargan de manejar numerosos repositorios remotos con los cuales pueden trabajar, de tal forma que puedes colaborar simultáneamente con diferentes grupos de personas en distintas maneras dentro del mismo proyecto. Esto permite establecer varios flujos de trabajo que no son posibles en sistemas centralizados, como pueden ser los modelos jerárquicos.

Características de git

Git maneja sus datos como un conjunto de copias instantáneas de un sistema de archivos miniatura. Cada vez que confirmas un cambio, o guardas el estado de tu proyecto en Git, él básicamente toma una foto del aspecto de todos tus archivos en ese momento y guarda una referencia a esa copia instantánea. Para ser eficiente, si los archivos no se han modificado Git no almacena el archivo de nuevo, sino un enlace al archivo anterior idéntico que ya tiene almacenado. Git maneja sus datos como una secuencia de copias instantáneas.

Copias instantánes, no diferenciadas

Git maneja sus datos como un conjunto de copias instantáneas de un sistema de archivos miniatura. Cada vez que confirmas un cambio, o guardas el estado de tu proyecto en Git, él básicamente toma una foto del aspecto de todos tus archivos en ese momento y guarda una referencia a esa copia instantánea. Para ser eficiente, si los archivos no se han modificado Git no almacena el archivo de nuevo, sino un enlace al archivo anterior idéntico que ya tiene almacenado. Git maneja sus datos como una secuencia de copias instantáneas.

Casi todas las operaciones son locales.

La mayoría de las operaciones en Git sólo necesitan archivos y recursos locales para funcionar. Por lo general no se necesita información de ningún otro computador de tu red. Por ejemplo, para navegar por la historia del proyecto, Git no necesita conectarse al servidor para obtener la historia y mostrártela - simplemente la lee directamente de tu base de datos local. Esto significa que ves la historia del proyecto casi instantáneamente. Esto también significa que hay muy poco que no puedes hacer si estás desconectado. Si te subes a un avión o a un tren y quieres trabajar un poco, puedes confirmar tus cambios felizmente hasta que consigas una conexión de red para subirlos.

Git tiene integridad

Todo en Git es verificado mediante una suma de comprobación (checksum en inglés) antes de ser almacenado, y es identificado a partir de ese momento mediante dicha suma. Esto significa que es imposible cambiar los contenidos de cualquier archivo o directorio sin que Git lo sepa.

Información

No puedes perder información durante su transmisión o sufrir corrupción de archivos sin que Git sea capaz de detectarlo.

El mecanismo que usa Git para generar esta suma de comprobación se conoce como hash SHA-1.

24b9da6552252987aa493b52f8696cd6d3b00373

Git sólo añade información

Cuando realizas acciones en Git, casi todas ellas sólo añaden información a la base de datos de Git. Es muy difícil conseguir que el sistema haga algo que no se pueda enmendar, o que de algún modo borre información.

Fuente. Pro GIt v2

Última actualización: 23.09.2025

Estados de git

Git tiene tres estados principales en los que se pueden encontrar tus archivos:

  1. Confirmado (committed): significa que los datos están almacenados de manera segura en tu base de datos local.
  2. Modificado (modified): significa que has modificado el archivo pero todavía no lo has confirmado a tu base de datos.
  3. Preparado (staged): significa que has marcado un archivo modificado en su versión actual para que vaya en tu próxima confirmación.

Esto nos lleva a las tres secciones principales de un proyecto de Git:

  1. El directorio de Git (Git directory) es donde se almacenan los metadatos y la base de datos de objetos para tu proyecto. Es la parte más importante de Git, y es lo que se copia cuando clonas un repositorio desde otra computadora.
  2. El directorio de trabajo (Working directory) es una copia de una versión del proyecto. Estos archivos se sacan de la base de datos comprimida en el directorio de Git, y se colocan en disco para que los puedas usar o modificar.
  3. El área de preparación (Staging area). Es un archivo, generalmente contenido en tu directorio de Git, que almacena información acerca de lo que va a ir en tu próxima confirmación.

El flujo de trabajo básico en Git es algo así:

  1. Modificas una serie de archivos en tu directorio de trabajo.
  2. Preparas los archivos, añadiéndolos a tu área de preparación.
  3. Confirmas los cambios, lo que toma los archivos tal y como están en el área de preparación y almacena esa copia instantánea de manera permanente en tu directorio de Git.

Fuente. Pro GIt v2

Última actualización: 23.09.2025

Instalación de git

Antes de empezar a utilizar Git, tienes que instalarlo en tu computadora.

Instalación en Linux

Si quieres instalar Git en Linux a través de un instalador binario, en general puedes hacerlo mediante la herramienta básica de administración de paquetes que trae tu distribución.

$ apt-get install git

Instalación en Mac

Instale homebrew si aún no lo tiene, luego ejecutar:

$ brew install git

Instalación en Windows

Solo tienes que visitar la página oficial, seleccionar la opción correspondiente y la descarga empezará automáticamente.

Fuente. Pro GIt v2

Última actualización: 23.09.2025

Fundamentos git

Nota importante

Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2

Subsecciones de Fundamentos git

Configuracion de git

Lo primero que deberás hacer cuando instales Git es establecer tu nombre de usuario y dirección de correo electrónico. Esto es importante porque los “commits” de Git usan esta información, y es introducida de manera inmutable en los commits que envías:

$ git config --global user.name "Sabela"
$ git config --global user.email "sabela@iessanclemente.net"

Sólo necesitas hacer esto una vez si especificas la opción –global, ya que Git siempre usará esta información para todo lo que hagas en ese sistema. Si quieres sobrescribir esta información con otro nombre o dirección de correo para proyectos específicos, puedes ejecutar el comando sin la opción –global cuando estés en ese proyecto.

Comprobando configuración

Si quieres comprobar tu configuración, puedes usar el comando git config –list para mostrar todas las propiedades que Git ha configurado:

$ git config --list
credential.helper=osxkeychain
user.email=sabela@iessanclemente.net
user.name=Sabela

También puedes comprobar el valor que Git utilizará para una clave específica ejecutando git config

$ git help <verbo>
$ git <verbo> --help 
Última actualización: 23.09.2025

Inicializando repositorio

Para inicializar el repositorio en local realizamos los siguientes pasos:

  1. Comprobamos la ruta en la que nos encontramos (comando pwd)
$ pwd
/Users/sabelasobrinosanmartin
  1. Nos movemos hasta el escritorio de mi usuario (comando cd)
$ cd Desktop 
  1. Creamos una carpeta que va a almacenar el repositorio (comando mkdir).
$ mkdir repositorio
  1. Nos movemos dentro de la carpeta que acabamos de crear (comando cd).
$ cd repositorio
  1. Inicializamos el repositorio dentro de esa carpeta. (comando git init).
$ git init .
ayuda: Usando 'master' como el nombre de la rama inicial. Este nombre de rama predeterminado
ayuda: está sujeto a cambios. Para configurar el nombre de la rama inicial para usar en todos
ayuda: de sus nuevos repositorios, reprimiendo esta advertencia, llama a:
ayuda: 
ayuda: 	git config --global init.defaultBranch <nombre>
ayuda: 
ayuda: Los nombres comúnmente elegidos en lugar de 'master' son 'main', 'trunk' y
ayuda: 'development'. Se puede cambiar el nombre de la rama recién creada mediante este comando:
ayuda: 
ayuda: 	git branch -m <nombre>
Inicializado repositorio Git vacío en /Users/sabelasobrinosanmartin/Desktop/repositorio/.git/

Nos fijamos en dos cosas, la primera es que al ejecutar el comando nos indica que se ha inicializado correctamente el repositorio en la ruta elegida.

Inicializado repositorio Git vacío en /Users/sabelasobrinosanmartin/Desktop/repositorio/.git/

La segunda es que a partir de ahora siempre que naveguemos dentro de nuestra carpeta tendremos al final de la ruta la versión en la que estamos trabajando (master) en este caso.

Si ejecutamos el comando ls –la (para que nos muestre los ficheros ocultos) veremos los siguientes:

$ ls -la
total 0
drwxr-xr-x   3 sabelasobrinosanmartin  staff    96  2 sep 11:37 .
drwx------@ 78 sabelasobrinosanmartin  staff  2496  3 sep 19:59 ..
drwxr-xr-x   9 sabelasobrinosanmartin  staff   288  2 sep 11:37 .git

Comprobamos que nos crea una carpeta .git que será la base del repositorio. En el caso de que queramos eliminar el repositorio lo único que tendremos que hacer es eliminar esa carpeta (comando rm).

$ rm -rf .git/

Una vez eliminada esa carpeta podemos ver que ya no nos muestra la versión en la que estamos trabajando (master).

Para poder trabajar con nuestro repositorio vamos a inicializarlo de nuevo y a crear dos nuevos ficheros con los comandos nano o vim:

$ git init
$ nano canciones.txt 
19 días y 500 noches, Joaquín Sabina
$ nano libros.txt
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón 
Última actualización: 23.09.2025

Cambios en el repositorio

Ya tenemos un repositorio Git y un checkout o copia de trabajo de los archivos de dicho proyecto. El siguiente paso es realizar algunos cambios y confirmar instantáneas de esos cambios en el repositorio cada vez que el proyecto alcance un estado que quieras conservar.

Git status

El comando git status es la herramienta principal para determinar qué archivos están en qué estado.

$ git status
En la rama master

No hay commits todavía

Archivos sin seguimiento:
  (usa "```bash add``` <archivo>..." para incluirlo a lo que se será confirmado)
	canciones.txt
	libros.txt
no hay nada agregado al commit pero hay archivos sin seguimiento presentes (usa "```bash add```" para hacerles seguimiento)

Puedes ver que los archivos canciones.txt y libros.txt están sin seguimiento porque aparece debajo del encabezado “Untracked files” (“Archivos no rastreados” en inglés) en la salida. Este estado, “Sin seguimiento” significa que Git ve archivos que no tenías antes. Git no los incluirá en tu próximo commit (próxima confirmación) a menos que se lo indiques explícitamente. Se comporta así para evitar incluir accidentalmente archivos binarios o cualquier otro archivo que no quieras incluir. Si queremos incluir los dos ficheros en el próximo commit debemos restrearlos.

Git add

Para comenzar a realizar un seguimiento de los archivos debes usar el comando bash add. Podemos ejecutar dos comandos uno por cada archivo.

$ git add canciones.txt
$ git add libros.txt

O realizar un único bash add . , que incluye todo lo que hay en la carpeta:

$ git add .

Si la salida produce algún mensaje de salida (los warnings) se debe a que estamos trabajando en una consola Linux dentro del sistema operativo de Windows.

Si ahora ejecutamos un git status para ver el estado de los archivos tendremos algo como esto:

$ git status
En la rama master

No hay commits todavía

Cambios a ser confirmados:
  (usa "git rm --cached <archivo>..." para sacar del área de stage)
	nuevos archivos: canciones.txt
	nuevos archivos: libros.txt

Puedes ver que está siendo rastreado porque aparece luego del encabezado “Cambios a ser confirmados” (“Changes to be committed” en inglés).

Si confirmas los cambios en este punto, se guardará en el historial la versión del archivo correspondiente al instante en que ejecutaste bash add, pudiendo volver al punto de partida del archivo siempre que quieras.

Git commit

Ahora que tu área de trabajo está como quieres, puedes confirmar tus cambios. Recuerda que cualquier cosa que no esté preparada -cualquier archivo que hayas creado o modificado y que no hayas agregado con bash add desde su edición- no será confirmado.

$ git commit -m "mensaje inicial" 
[master (commit-raíz) 2823aec] mensaje inicial
 2 files changed, 2 insertions(+)
 create mode 100644 canciones.txt
 create mode 100644 libros.txt

A través del comando –m le indicamos el mensaje que veremos después.

Puedes ver que la confirmación te devuelve una salida descriptiva: indica cuál rama has confirmado (master), que checksum SHA-1 tiene el commit (2823aec), cuántos archivos han cambiado y estadísticas sobre las líneas añadidas y eliminadas en el commit.

Recuerda que la confirmación guarda una instantánea de tu área de trabajo. Todo lo que no hayas preparado sigue allí modificado; puedes hacer una nueva confirmación para añadirlo a tu historial. Cada vez que realizas un commit, guardas una instantánea de tu proyecto la cual puedes usar para comparar o volver a ella luego.

Última actualización: 23.09.2025

Archivos Ignorados

A veces, tendrás algún tipo de archivo que no quieres que Git añada automáticamente o que ni siquiera quieras que aparezca como no rastreado. Este suele ser el caso de archivos generados automáticamente como trazas o archivos creados por tu sistema de compilación. En estos casos, puedes crear un archivo llamado .gitignore que liste patrones o archivos a considerar. Para ver esto, vamos a crearnos un nuevo archivo que queremos que sea ignorado:

$ nano system.log

Y vamos a modificar canciones.txt añadiendo una nueva línea:

$ nano libros.txt
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón 

Si ahora hacemos un git status comprobamos que tenemos dos archivos, uno modificado (libros.txt) y el otro sin realizar ningún seguimiento (system.log)

$ git status
En la rama master
Cambios no rastreados para el commit:
  (usa "git add <archivo>..." para actualizar lo que será confirmado)
  (usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
	modificados:     libros.txt

Archivos sin seguimiento:
  (usa "git add <archivo>..." para incluirlo a lo que se será confirmado)
	system.log

sin cambios agregados al commit (usa "git add" y/o "git commit -a")

Lo que vamos a hacer es crear un fichero .gitignore y añadirle el fichero system.log para que no se realice ningún seguimiento sobre él, ya que no nos interesa.

$ nano .gitignore
system.log

Ahora mismo tenemos los siguientes ficheros en nuestra carpeta:

$ ls -la
total 32
drwxr-xr-x   7 sabelasobrinosanmartin  staff   224  3 sep 23:45 .
drwx------@ 78 sabelasobrinosanmartin  staff  2496  3 sep 19:59 ..
drwxr-xr-x  12 sabelasobrinosanmartin  staff   384  3 sep 23:42 .git
-rw-r--r--   1 sabelasobrinosanmartin  staff    11  3 sep 23:45 .gitignore
-rw-r--r--   1 sabelasobrinosanmartin  staff     2  3 sep 23:26 canciones.txt
-rw-r--r--   1 sabelasobrinosanmartin  staff    43  3 sep 23:42 libros.txt
-rw-r--r--   1 sabelasobrinosanmartin  staff     4  3 sep 23:41 system.log

Si ahora hacemos un git status vemos que tenemos el fichero libros.txt modificado y el .gitignore para realizar el seguimiento pero ya no tenemos system.log porque lo hemos añadido al fichero .gitignore:

$ git status
En la rama master
Cambios no rastreados para el commit:
  (usa "git add <archivo>..." para actualizar lo que será confirmado)
  (usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
	modificados:     libros.txt

Archivos sin seguimiento:
  (usa "git add <archivo>..." para incluirlo a lo que se será confirmado)
	.gitignore

sin cambios agregados al commit (usa “git add” y/o “git commit -a”)

Vamos a hacer un bash add sobre esos dos ficheros para marcarlos:

En este momento tenemos los dos ficheros rastreados y listos para su confirmación:

$ git add . 

Realizamos un commit para confirmar los cambios:

$ git commit -m "añadido gitignore"
[master 0d8d651] añadido gitignore
 2 files changed, 2 insertions(+), 1 deletion(-)
 create mode 100644 .gitignore

Patrones para ignorar ficheros

Ya vimos el funcionamiento del archivo .gitignore, en el que podemos añadir los nombres de los ficheros que no queremos que sean rastreados, pero también podemos añadir patrones de rastreo, lo que nos permitirá crear reglas y no tener que añadir todos los nombres de los ficheros.

Las reglas sobre los patrones que puedes incluir en el archivo .gitignore son las siguientes:

  • Ignorar las líneas en blanco y aquellas que comiencen con #.
  • Aceptar patrones glob estándar.
  • Los patrones pueden terminar en barra (/) para especificar un directorio.
  • Los patrones pueden negarse si se añade al principio el signo de exclamación (!).
  • Los patrones glob son una especie de expresión regular simplificada usada por los terminales.
  • Un asterisco (*) corresponde a cero o más caracteres.
  • [abc] corresponde a cualquier caracter dentro de los corchetes (en este caso a, b o c).
  • El signo de interrogación (?) corresponde a un caracter cualquiera-
  • Los corchetes sobre caracteres separados por un guión ([0-9]) corresponde a cualquier caracter entre ellos (en este caso del 0 al 9).
  • También puedes usar dos asteriscos para indicar directorios anidados; a/**/z coincide con a/z, a/b/z, a/b/c/z, etc.

Referencias

Última actualización: 23.09.2025

Git checkout

Con el comando checkout podemos volver a una versión anterior de nuestro documento. En este caso vamos a revertir los cambios en libros.txt eliminando la línea nueva del último commit. Actualmente el archivo está como sigue:

1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón 

Si queremos volver a la versión anterior en la cual sólo estaba la línea “1984, George Orwell” del primer commit, lo primero que tenemos que hacer es un git log. Con este comando podemos ver el hash (ese número tan largo) de cada commit:

$ git log
commit 0d8d651169c5db86505bec68c735f8fb4d8772e0 (HEAD -> master)
Author: Sabela <sabela@iessanclemente.net>
Date:   Sat Sep 3 23:47:58 2022 +0200

    añadido gitignore

commit 2823aec8facffe32d228a02c4af509832dbd0f70
Author: Sabela <sabela@iessanclemente.net>
Date:   Sat Sep 3 23:38:25 2022 +0200

    mensaje inicial

Para revertir los cambios tenemos que copiar los 10 primeros números del commit donde está la versión de nuestro documento que nos interesa (en este caso mensaje inicial) y pegarlos juntos al comando bashcheckout:

git checkout 2823aec8fac
Nota: cambiando a '2823aec8fac'.

Te encuentras en estado 'detached HEAD'. Puedes revisar por aquí, hacer
cambios experimentales y hacer commits, y puedes descartar cualquier
commit que hayas hecho en este estado sin impactar a tu rama realizando
otro checkout.

HEAD está ahora en 2823aec mensaje inicial

Si después de realizar el checkout comprobamos el contenido del fichero (canciones.txt) vemos que ha sido modificado y que además nos cambia la versión en la que estamos trabajando ya no es la master sino una antigua.

$ more libros.txt
1984, George Orwell

Como hemos revertido todos los cambios, si hacemos un ls –la ya no veremos el archivo .gitignore

$ ls -la
total 24
drwxr-xr-x   6 sabelasobrinosanmartin  staff   192  3 sep 23:55 .
drwx------@ 78 sabelasobrinosanmartin  staff  2496  3 sep 19:59 ..
drwxr-xr-x  12 sabelasobrinosanmartin  staff   384  3 sep 23:55 .git
-rw-r--r--   1 sabelasobrinosanmartin  staff     2  3 sep 23:26 canciones.txt
-rw-r--r--   1 sabelasobrinosanmartin  staff     2  3 sep 23:55 libros.txt
-rw-r--r--   1 sabelasobrinosanmartin  staff     4  3 sep 23:41 system.log

Para volver a la rama master simplemente hacemos un checkout a la rama master

$ git checkout master 
Última actualización: 23.09.2025

Git Log

Después de haber hecho varias confirmaciones, probablemente quieras mirar atrás para ver qué modificaciones se han llevado a cabo. La herramienta más básica y potente para hacer esto es el comando git log. Si hacemos un git log en este momento tendremos las siguientes líneas de confirmación:

$ git log
commit 0d8d651169c5db86505bec68c735f8fb4d8772e0 (HEAD -> master)
Author: Sabela <sabela@iessanclemente.net>
Date:   Sat Sep 3 23:47:58 2022 +0200

    añadido gitignore

commit 2823aec8facffe32d228a02c4af509832dbd0f70
Author: Sabela <sabela@iessanclemente.net>
Date:   Sat Sep 3 23:38:25 2022 +0200

    mensaje inicial
Última actualización: 23.09.2025

Archivos modificados

Vamos a cambiar un archivo que esté rastreado. Nos vale cualquier archivo, vamos a modificar el archivo libros.txt:

$ nano libros.txt 
1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
la cuidad y los perros, Mario Vargas Llosa

Si ahora ejecutamos un git status veremos algo parecido a esto:

$ git status
En la rama master
Cambios no rastreados para el commit:
  (usa "git add <archivo>..." para actualizar lo que será confirmado)
  (usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
	modificados:     libros.txt

sin cambios agregados al commit (usa "git add" y/o "git commit -a")

El archivo “libros.txt” aparece en una sección llamada “Cambios no rastreados para el commit” - lo que significa que existe un archivo rastreado que ha sido modificado en el directorio de trabajo pero que aún no está preparado. Para prepararlo, ejecutamos el comando git add.

$ git add . 

Al ejecutar un git status comprobamos que todo está correcto:

$ git status 
En la rama master
Cambios a ser confirmados:
  (usa "git restore --staged <archivo>..." para sacar del área de stage)
	modificados:     libros.txt

El archivo está preparados y formará parte de tu próxima confirmación. En este momento, supongamos que recuerdas que debes hacer un pequeño cambio en canciones.txt antes de confirmarlo. Abres de nuevo el archivo, lo cambias y ahora estás listo para confirmar.

1984, George Orwell
la sombra del viento, Carlos Ruíz Zafón
la cuidad y los perros, Mario Vargas Llosa
El libro negro de las horas, Eva García Sáenz de Urturi

Sin embargo, ejecutemos git status una vez más:

$ git status
En la rama master
Cambios a ser confirmados:
  (usa "git restore --staged <archivo>..." para sacar del área de stage)
	modificados:     libros.txt

Cambios no rastreados para el commit:
  (usa "git add <archivo>..." para actualizar lo que será confirmado)
  (usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
	modificados:     libros.txt

Ahora libros.txt aparece como preparado y no preparado. Resulta que Git prepara un archivo de acuerdo al estado que tenía cuando ejecutas el comando git add. Si confirmas ahora, se confirmará la versión de libros.txt que tenías la última vez que ejecutaste git add y no la versión que ves ahora en tu directorio de trabajo al ejecutar git status. Si modificas un archivo luego de ejecutar git add, deberás ejecutar git add de nuevo para preparar la última versión del archivo:

$ git add .
$ git status
En la rama master
Cambios a ser confirmados:
  (usa "git restore --staged <archivo>..." para sacar del área de stage)
	modificados:     libros.txt

$ git commit -m "archivos modificados"
[master fcff217] archivos modificados
 1 file changed, 3 insertions(+)
Última actualización: 23.09.2025

Archivos modificados

Estado abreviado

Si bien es cierto que la salida de git status es bastante explícita, también es verdad que es muy extensa. Podemos usar el siguiente comando:

$ git status --short 

Que nos ofrece una salida abreviada bajo las siguientes siglas:

  • Archivo modificado (M)
  • Archivo no rastreado (??)
  • Archivo preparado (A).

Ver cambios no preparados

Vamos a modificar el archivo canciones.txt:

$ nano canciones.txt 
19 días y 500 noches, Joaquín Sabina
Por la raja de tu falda, Estopa

Para ver qué has cambiado pero aún no has preparado, escribe git diff sin más parámetros:

$ git diff
diff --git a/canciones.txt b/canciones.txt
index 7898192..32c4828 100644
--- a/canciones.txt
+++ b/canciones.txt
@@ -1 +1,2 @@
-a
+Por la raja de tu falda, Estopa

Nos dice que hemos modificado el archivo canciones.txt añadiendo la línea “Por la raja de tu falda, Estopa”. Ahora vamos a preparar y confirmar todos los cambios:

$ git add . 

Y confirmamos:

$ git commit -m "añadida cancion"
[master 8671097] añadida cancion
 1 file changed, 2 insertions(+), 1 deletion(-)

Vamos a modificar ahora el libros.txt

$ nano libros.txt
1984, George Orwell
la cuidad y los perros, Mario Vargas Llosa
El libro negro de las horas, Eva García Sáenz de Urturi
Marina, Carlos Ruíz Zafón

Si ejecutamos un git diff nos indica que una línea ha sido borrada (rojo) y otra ha sido añadida (verde).

$ git diff
diff --git a/libros.txt b/libros.txt
index e808bd3..1810eb8 100644
--- a/libros.txt
+++ b/libros.txt
@@ -1,4 +1,4 @@
 1984, George Orwell
-la sombra del viento, Carlos Ruíz Zafón 
 la cuidad y los perros, Mario Vargas Llosa
 El libro negro de las horas, Eva García Sáenz de Urturi
+Marina, Carlos Ruíz Zafón

Si quieres ver lo que has preparado y será incluido en la próxima confirmación, puedes usar git diff --staged. Este comando compara tus cambios preparados con la última instantánea confirmada. Si ejecutamos ahora git diff –staged no veremos nada (ya que no hay nada preparado).

$ git diff --staged

Si ahora ejecutamos un git diff no veremos nada, puesto que los cambios ya están preparados. Es importante resaltar que al llamar a git diff sin parámetros no verás los cambios desde tu última confirmación - solo verás los cambios que aún no están preparados. Esto puede ser confuso porque si preparas todos tus cambios, git diff no te devolverá ninguna salida.

Última actualización: 23.09.2025

Opciones con archivos

Eliminar archivos

Si simplemente eliminas el archivo de tu directorio de trabajo, aparecerá en la sección “Changes not staged for commit” (esto es, sin preparar) en la salida de git status. Vamos a eliminar el fichero canciones.txt:

$  rm canciones.txt

Si hacemos un git status, comprobamos que el archivo ha sido marcado para eliminar:

$ git status
En la rama master
Cambios no rastreados para el commit:
  (usa "git add/rm <archivo>..." para actualizar a lo que se le va a hacer commit)
  (usa "git restore <archivo>..." para descartar los cambios en el directorio de trabajo)
	borrados:        canciones.txt

sin cambios agregados al commit (usa "git add" y/o "git commit -a")

Ahora, si ejecutas git rm, entonces se prepara la eliminación del archivo:

$ git rm canciones.txt
rm 'canciones.txt'

Con la próxima confirmación, el archivo habrá desaparecido y no volverá a ser rastreado.

$ git status
En la rama master
Cambios a ser confirmados:
  (usa "git restore --staged <archivo>..." para sacar del área de stage)
	borrados:        canciones.txt

Dejar de rastrear archivos

Si ahora modificamos el archivo “nuevo archivo.txt”

Nos aparece como archivo sin seguimiento:

Cambiar el nombre archivos

Para cambiar el nombre de un archivo tenemos el siguiente comando:

$ git mv canciones.txt peliculas.txt 
Última actualización: 23.09.2025

Historial de confirmaciones

Opciones del historial de confirmaciones

Después de haber hecho varias confirmaciones, o si has clonado un repositorio que ya tenía un histórico de confirmaciones, probablemente quieras mirar atrás para ver qué modificaciones se han llevado a cabo. La herramienta más básica y potente para hacer esto es el comando git log.

$ git log

Se ven las confirmaciones en orden cronológico inverso. Una de las opciones más útiles es -p, que muestra las diferencias introducidas en cada confirmación. También puedes usar la opción -2, que hace que se muestren únicamente las dos últimas entradas del historial:

$ git log -p -2

Limitar la salida

las opciones temporales como –since (desde) y –until (hasta) resultan muy útiles. Por ejemplo, este comando lista todas las confirmaciones hechas durante las dos últimas semanas:

$ git log --since=2.weeks

Deshacer

En cualquier momento puede que quieras deshacer algo. Uno de las acciones más comunes a deshacer es cuando confirmas un cambio antes de tiempo y olvidas agregar algún archivo, o te equivocas en el mensaje de confirmación. Si quieres rehacer la confirmación, puedes reconfirmar con la opción –amend.

$ git commit --ammend -m "nuevo commit que modifica el anterior"
Última actualización: 23.09.2025

Remotos

Los repositorios remotos son versiones de tu proyecto que están hospedadas en Internet o en cualquier otra red. Puedes tener varios de ellos, y en cada uno tendrás generalmente permisos de solo lectura o de lectura y escritura. Colaborar con otras personas implica gestionar estos repositorios remotos enviando y trayendo datos de ellos cada vez que necesites compartir tu trabajo.

Obtener un repositorio remoto

Para obtener un repositorio remoto debemos usar el comando git clone junto con la url del repositorio:

$ git clone https://gitlab.iessanclemente.net/sabela/contornos-desarrollo.git

Ver repositorios remotos

Para ver los repositorios remotos que tienes configurados, debes ejecutar el comando git remote dentro de la carpeta del repositorio.

Si has clonado tu repositorio, deberías ver al menos origin (origen, en inglés) - este es el nombre que por defecto Git le da al servidor del que has clonado.

$ git remote
origin

También puedes pasar la opción -v, la cual muestra las URLs que Git ha asociado al nombre y que serán usadas al leer y escribir en ese remoto:

$ git remote -v
origin	https://gitlab.iessanclemente.net/sabela/contornos-desarrollo.git (fetch)
origin	https://gitlab.iessanclemente.net/sabela/contornos-desarrollo.git (push)

Si tienes más de un remoto, el comando los listará todos.

Añadir repositorios remotos

Para añadir un remoto nuevo y asociarlo a un nombre que puedas referenciar fácilmente, ejecuta git remote add [nombre] [url]:

$ git remote add daw1 https://gitlab.iessanclemente.net/dwcs
$ git remote -v
origin	https://gitlab.iessanclemente.net/sabela/contornos-desarrollo.git (fetch)
origin	https://gitlab.iessanclemente.net/sabela/contornos-desarrollo.git (push)
daw1 https://gitlab.iessanclemente.net/dwcs (fetch)
daw1 https://gitlab.iessanclemente.net/dwcs (push)

A partir de ahora puedes usar el nombre daw1 en la línea de comandos en lugar de la URL entera.

Traer y Combinar Remotos

Para obtener datos de tus proyectos remotos puedes ejecutar:

$ git fetch [remote-name]

El comando irá al proyecto remoto y se traerá todos los datos que aun no tienes de dicho remoto. Luego de hacer esto, tendrás referencias a todas las ramas del remoto, las cuales puedes combinar e inspeccionar cuando quieras.

$ git fetch origin

Este comando se trae todo el trabajo nuevo que ha sido enviado a ese servidor desde que lo clonaste (o desde la última vez que trajiste datos). Es importante destacar que el comando git fetch solo trae datos a tu repositorio local, ni lo combina automáticamente con tu trabajo ni modifica el trabajo que llevas hecho. La combinación con tu trabajo debes hacerla manualmente cuando estés listo.

Enviar a tus remotos

Cuando tienes un proyecto que quieres compartir, debes enviarlo a un servidor. El comando para hacerlo es simple: git push [nombre-remoto] [nombre-rama]. Si quieres enviar tu rama master a tu servidor origin, entonces puedes ejecutar el siguiente comando y se enviarán todos los commits que hayas hecho al servidor:

$ git push origin master
remotos

Este comando solo funciona si clonaste de un servidor sobre el que tienes permisos de escritura y si nadie más ha enviado datos por el medio. Si alguien más clona el mismo repositorio que tú y envía información antes que tú, tu envío será rechazado. Tendrás que traerte su trabajo y combinarlo con el tuyo antes de que puedas enviar datos al servidor.

Eliminar y renombrar remotos

Si quieres cambiar el nombre de la referencia de un remoto puedes ejecutar git remote rename. Por ejemplo, si quieres cambiar el nombre de daw1 a daw2:

$ git remote rename daw1 daw2

Si por alguna razón quieres eliminar un remoto puedes usar git remote rm:

$ git remote rm daw1
Gitlab

En el vídeo se muestra un repositorio remoto con Bitbucket. Este repositorio es similar al que tenemos nosotros/as en el Gitlab. Simplemente cambia la interfaz.

Etiquetado

Git tiene la posibilidad de etiquetar puntos específicos del historial como importantes. Esta funcionalidad se usa típicamente para marcar versiones de lanzamiento (v1.0, por ejemplo).

Listar Tus Etiquetas

Listar las etiquetas disponibles en Git es sencillo. Simplemente escribe git tag:

$ git tag
v0.1
v1.3
Información

Este comando lista las etiquetas en orden alfabético; el orden en el que aparecen no tiene mayor importancia.

Crear etiquetas

Para crear una etiqueta podemos usar el comando git tag.

$ git tag -a v1.4 -m 'my version 1.4'
$ git tag
v0.1
v1.3
v1.4

La opción -m especifica el mensaje de la etiqueta, el cual es guardado junto con ella. Si no especificas el mensaje de una etiqueta anotada, Git abrirá el editor de texto para que lo escribas.

Puedes ver la información de la etiqueta junto con el commit que está etiquetado al usar el comando git show:

$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date:   Sat May 3 20:19:12 2014 -0700

my version 1.4

commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

Compartir etiquetas

Por defecto, el comando git push no transfiere las etiquetas a los servidores remotos. Debes enviar las etiquetas de forma explícita al servidor luego de que las hayas creado. Este proceso es similar al de compartir ramas remotas - puedes ejecutar git push origin [etiqueta].

$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
 * [new tag]         v1.5 -> v1.5

Ramas Git

Nota importante

Todos los contenidos sobre GIT son basados en el libro oficial Pro GIt v2

Subsecciones de Ramas Git

Ramificación

Git almacena los datos como una serie de instantáneas (copias puntuales de los archivos completos, tal y como se encuentran en ese momento). En cada confirmación de cambios (commit), Git almacena una instantánea de tu trabajo preparado. Dicha instantánea contiene además unos metadatos con el autor y el mensaje explicativo, y uno o varios apuntadores a las confirmaciones (commit).

Vamos a realizar un ejemplo y para ello creamos un nuevo repositorio:

$ mkdir ramas
$ cd ramas/
$ git init .
Initialized empty Git repository in /home/sanclemente.local/sabela/Escritorio/ramas/.git/

Una vez dentro vamos a crear tres ficheros:

$ nano fichero1.txt
$ nano fichero2.txt
$ nano fichero3.txt

Ahora vamos a preparar y a confirmar todos ellos:

$ git add .
$ git commit -m "commit inicial"
[master (root-commit) 7b6988f] commit inicial
 Committer: sabela <sabela@infol03.sanclemente.local>
 3 files changed, 3 insertions(+)
 create mode 100644 fichero1.txt
 create mode 100644 fichero2.txt
 create mode 100644 fichero3.txt

Cuando creas una confirmación con el comando git commit, Git realiza sumas de control de cada subdirectorio, y las guarda como objetos árbol en el repositorio Git. Después, Git crea un objeto de confirmación con los metadatos pertinentes y un apuntador al objeto árbol raíz del proyecto.

Si haces más cambios y vuelves a confirmar, la siguiente confirmación guardará un apuntador a su confirmación precedente.

Una rama Git es simplemente un apuntador móvil apuntando a una de esas confirmaciones. La rama por defecto de Git es la rama master o la rama main. Con la primera confirmación de cambios que realicemos, se creará esta rama principal master apuntando a dicha confirmación. En cada confirmación de cambios que realicemos, la rama irá avanzando automáticamente.

Información

La rama “master” en Git, no es una rama especial. Es como cualquier otra rama. La única razón por la cual aparece en casi todos los repositorios es porque es la que crea por defecto el comando git init y la gente no se molesta en cambiarle el nombre.

Crear nueva rama

Cuando creamos una nueva rama simplemente se crea un nuevo apuntador para que lo puedas mover libremente. Por ejemplo, supongamos que quieres crear una rama nueva denominada “testing”. Para ello, usarás el comando git branch:

$ git branch testing

Esto creará un nuevo apuntador apuntando a la misma confirmación donde estés actualmente.

Git sabe en qué rama estás en este momento mediante un apuntador especial denominado HEAD que apunta a la rama local en la que tú estés en ese momento, en este caso la rama master; pues el comando git branch solamente crea una nueva rama, pero no salta a dicha rama.

Cambiar de rama

Para saltar de una rama a otra, tienes que utilizar el comando git checkout. Hagamos una prueba, saltando a la rama testing recién creada:

$ git checkout testing

Esto mueve el apuntador HEAD a la rama testing.

¿Cuál es el significado de todo esto? lo veremos tras realizar otra confirmación de cambios:

$ nano fichero1.txt
$ git commit -a -m 'cambio en fichero 1'

Observamos algo interesante: la rama testing avanza, mientras que la rama master permanece en la confirmación donde estaba cuando lanzaste el comando git checkout para saltar. Volvamos ahora a la rama master:

$ git checkout master

Este comando realiza dos acciones: Mueve el apuntador HEAD de nuevo a la rama master, y revierte los archivos de tu directorio de trabajo; dejándolos tal y como estaban en la última instantánea confirmada en dicha rama master. Esto supone que los cambios que hagas desde este momento en adelante, divergirán de la antigua versión del proyecto. Básicamente, lo que se está haciendo es rebobinar el trabajo que habías hecho temporalmente en la rama testing; de tal forma que puedas avanzar en otra dirección diferente.

Ramas

Es importante destacar que cuando saltas a una rama en Git, los archivos de tu directorio de trabajo cambian. Si saltas a una rama antigua, tu directorio de trabajo retrocederá para verse como lo hacía la última vez que confirmaste un cambio en dicha rama. Si Git no puede hacer el cambio limpiamente, no te dejará saltar.

Haz algunos cambios más y confírmalos:

$ nano fichero2.txt
$ git commit -a -m 'cambio en fichero 2'

Ahora el historial de tu proyecto diverge. Has creado una rama y saltado a ella, has trabajado sobre ella; has vuelto a la rama original, y has trabajado también sobre ella. Los cambios realizados en ambas sesiones de trabajo están aislados en ramas independientes: puedes saltar libremente de una a otra según estimes oportuno. Y todo ello simplemente con tres comandos: git branch, git checkout y git commit.

Última actualización: 23.09.2025

Ramas Remotas

Las ramas remotas son referencias al estado de las ramas en tus repositorios remotos. Son ramas locales que no puedes mover; se mueven automáticamente cuando estableces comunicaciones en la red. Las ramas remotas funcionan como marcadores, para recordarte en qué estado se encontraban tus repositorios remotos la última vez que conectaste con ellos. Suelen referenciarse como (remoto)/(rama).

Publicar

Cuando quieres compartir una rama con el resto del mundo, debes llevarla (push) a un remoto donde tengas permisos de escritura. Tus ramas locales no se sincronizan automáticamente con los remotos en los que escribes, sino que tienes que enviar (push) expresamente las ramas que desees compartir. De esta forma, puedes usar ramas privadas para el trabajo que no deseas compartir, llevando a un remoto tan solo aquellas partes que deseas aportar a los demás.

Traer y Fusionar

A pesar de que el comando git fetch trae todos los cambios que no tienes del servidor, este no modifica tu directorio de trabajo. Simplemente obtendrá los datos y dejará que tú mismo los fusiones. Sin embargo, existe un comando llamado git pull, el cuál básicamente hace git fetch seguido por git merge en la mayoría de los casos. Si tienes una rama de seguimiento configurada como vimos en la última sección, bien sea asignándola explícitamente o creándola mediante los comandos clone o checkout, git pull identificará a qué servidor y rama remota sigue tu rama actual, traerá los datos de dicho servidor e intentará fusionar dicha rama remota.

Normalmente es mejor usar los comandos fetch y merge de manera explícita pues la magia de git pull puede resultar confusa.

Eliminar Ramas Remotas

Imagina que ya has terminado con una rama remota, es decir, tanto tú como tus colaboradores habéis completado una determinada funcionalidad y la habéis incorporado (merge) a la rama master en el remoto (o donde quiera que tengáis la rama de código estable). Puedes borrar la rama remota utilizando la opción –delete de git push.

Última actualización: 23.09.2025

Claves SSH

Como habréis observado, cada vez que hacemos un git push nos pide el usuario y contraseña. Esto es bastante molesto.

Una forma de evitar esto es mediante un par de claves SSH (una clave privada y una clave pública). Ambas se complementa. La una sin la otra no sirve de nada.

Este método evita que nuestro usuario y contraseña de GitHub se guarde en un archivo de disco. Por tanto es muy seguro. En caso de que alguién haga login en nuestro PC podría acceder a nuestras claves. En dicho caso eliminaríamos el par de claves y volveríamos a crear unas nuevas y nuestro usuario y contraseña de GitHub nunca se verían comprometidos.

Vamos a seguir los siguientes pasos:

1. Generamos un par de claves SSH

Es muy sencillo. Como usuario normal (sin ser root) ejecutamos el comando

ssh-keygen

La salida será algo parecido a esto:

Generating public/private rsa key pair.
Enter file in which to save the key (/home/sanclemente.local/sabela/.ssh/id_rsa): 
/home/sanclemente.local/sabela/.ssh/id_rsa already exists.
Overwrite (y/n)? y
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/sanclemente.local/sabela/.ssh/id_rsa
Your public key has been saved in /home/sanclemente.local/sabela/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:DA2VOPu52mR9ZJX1PzXeKEbXltjNNoSRt0qC+LOA0sM sabela@a26eql00
The key's randomart image is:
+---[RSA 3072]----+
|      .o..   .+..|
|      oo.    o+*+|
|      .oo . ..==X|
|      .+ . o +.=*|
|    o ..S.  B ooo|
|   . E .o+ + o  .|
|    . . +.+ .    |
|       +.. .     |
|      ...        |
+----[SHA256]-----+

Pulsamos Intro a todo. En el caso de que ya exista un par de claves nos preguntará si deseamos sobreescribir (Override (y/n)? ). Si queremos proteger nuestra clave privada podemos poner una contraseña.

Esto nos creará una carpeta ~/.ssh y dentro al menos 2 archivos:

sabela@a26eql00:~/.ssh$ ls 
id_rsa  id_rsa.pub  known_hosts  known_hosts.old

El primer archivo corresponde a la clave privada y el segundo a la clave pública.

Copiamos el contenido de la clave pública en un editor de texto. Nos hará falta más adelante.

ssh-rsa AAAAaasdasd2B3NzaC1yc2EAAAADAQABAAABgQC9gkz1NnIv6WOGHwuYZk9yXPvRyAF5f5SjLlK5PdhOP1NKs8kvXPWjRR6o9UJvzzTr/f7i2AJEHYGCJX8FXTNULFfEFwnCRGZ4LVxts7Tf5LTeJ2U2wSHxxHiQTXVu4M3Z7HuN1DLti3ioB9Yf/bPZopkhkn/KOBl/UngeQV2lHLW
v8aeFC72L8UDlW8xy46nr1Yk5bsIS6OSKwEsLzthBkZmkwU1Zw7lLPwexOByyD80cm9qGzCe8IEF+zM9wSiYdJ1VLNGnNqMhcCMB5nmM/ipo7MzZGd/K0BT4iu5wuRJUutIDySASMCjLuuLOnJd24Rsux0M9GBp2AJCWKyETrLVFBee2F5ky+HhuuCnER6yocgVZIn0vOtwLBFEUOXU
3zt+8YuCefLvphXqPAuW3FIptROpOYc+PxABDGuUDQopNIGuNiztUdmej/DXVeeFMmdN/ScOIouE+GS6hl9ce1AyUVaQMfzeK78TwyqdwQlM0ReXQurCUQRHbSGUdeILM= sabela@a26eql00

Debe copiarse ssh-rsa …. sabela@a26eql00

En vuestro caso, en lugar de sabela@a26eql00 aparecerá otro usuario y pc.

2. Añadimos clave ssh pública a github.

Iniciamos sesión de GitHub y en el menú general (esquina superior derecha) seleccionamos la opción Settings.

github settings github settings

Luego, en la parte izquierda, elegimos la opción SSH y GPG keys

github ssh gpg github ssh gpg

A continuación, a la derecha, pulsamos en el botón New SSH key

github new ssh key github new ssh key

Luego ponemos un nombre a la clave, por ejemplo pc-casa. Y copiamos el contenido de la clave pública. Finalmente, pulsamos en el botón Add SSH key

github add ssh key github add ssh key

La clave anterior puede usarse para cualquiera de nuestros repositorios. Para hacer uso de ella, lo único que necesitamos es la URL en formato SSH de cada repositorio.

3. Asociando nuestro repositorio local mediante SSH

Nuestro repositorio local estaba asociado a origin mediante HTTPS. Debemos dar de baja dicho enlace y crear uno nuevo que haga uso del protocolo SSH.

Ejecutamos

git remote remove origin

git remote add origin git@github.com: tu_usuario/tu_repositorio